| 12 | * Manages link detection across multiple providers with intelligent caching |
| 13 | */ |
| 14 | export class LinkDetector { |
| 15 | private providers: ILinkProvider[] = []; |
| 16 | |
| 17 | // Cache links by hyperlink_id for fast lookups |
| 18 | // Key format: `h${hyperlinkId}` for OSC 8 links |
| 19 | // Key format: `r${row}:${startX}-${endX}` for regex links (future) |
| 20 | private linkCache = new Map<string, ILink>(); |
| 21 | |
| 22 | // Track which rows have been scanned to avoid redundant provider calls |
| 23 | private scannedRows = new Set<number>(); |
| 24 | |
| 25 | // Terminal instance for buffer access |
| 26 | constructor(private terminal: ITerminalForLinkDetector) {} |
| 27 | |
| 28 | /** |
| 29 | * Register a link provider |
| 30 | */ |
| 31 | registerProvider(provider: ILinkProvider): void { |
| 32 | this.providers.push(provider); |
| 33 | this.invalidateCache(); // New provider may detect different links |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Get link at the specified buffer position |
| 38 | * @param col Column (0-based) |
| 39 | * @param row Absolute row in buffer (0-based) |
| 40 | * @returns Link at position, or undefined if none |
| 41 | */ |
| 42 | async getLinkAt(col: number, row: number): Promise<ILink | undefined> { |
| 43 | // First, check if this cell has a hyperlink_id (fast path for OSC 8) |
| 44 | const line = this.terminal.buffer.active.getLine(row); |
| 45 | if (!line || col < 0 || col >= line.length) { |
| 46 | return undefined; |
| 47 | } |
| 48 | |
| 49 | const cell = line.getCell(col); |
| 50 | if (!cell) { |
| 51 | return undefined; |
| 52 | } |
| 53 | const hyperlinkId = cell.getHyperlinkId(); |
| 54 | |
| 55 | if (hyperlinkId > 0) { |
| 56 | // Fast path: check cache by hyperlink_id |
| 57 | const cacheKey = `h${hyperlinkId}`; |
| 58 | if (this.linkCache.has(cacheKey)) { |
| 59 | return this.linkCache.get(cacheKey); |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | // Slow path: scan this row if not already scanned |
| 64 | if (!this.scannedRows.has(row)) { |
| 65 | await this.scanRow(row); |
| 66 | } |
| 67 | |
| 68 | // Check cache again (hyperlinkId or position-based) |
| 69 | if (hyperlinkId > 0) { |
| 70 | const cacheKey = `h${hyperlinkId}`; |
| 71 | const link = this.linkCache.get(cacheKey); |
nothing calls this directly
no outgoing calls
no test coverage detected
searching dependent graphs…