export interface Mention { name: string; start: number; end: number; } // Scan prose only: escaped characters, inline code and fenced code are literal. // The same scanner powers submission and completion so their boundaries agree. export function findMentions(text: string, partial = false): Mention[] { const mentions: Mention[] = []; let fence: { char: string; length: number } | undefined; let inlineTicks = 0; for (let i = 0; i < text.length;) { if (i === 0 || text[i - 1] === "\n") { const lineEnd = text.indexOf("\n", i); const end = lineEnd < 0 ? text.length : lineEnd; const line = text.slice(i, end); const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line); if (fence) { if (marker && marker[1][0] === fence.char && marker[1].length >= fence.length && !marker[2].trim()) { fence = undefined; } i = end < text.length ? end + 1 : end; continue; } if (marker && inlineTicks === 0) { fence = { char: marker[1][0], length: marker[1].length }; i = end < text.length ? end + 1 : end; continue; } } if (text[i] === "`") { let end = i + 1; while (text[end] === "`") end++; const length = end - i; if (inlineTicks === 0) inlineTicks = length; else if (inlineTicks === length) inlineTicks = 0; i = end; continue; } if (inlineTicks) { i++; continue; } if (text[i] === "\\") { i += 2; continue; } if (text[i] === "$" && (i === 0 || !/[a-zA-Z0-9_$/\\.\-]/.test(text[i - 1]))) { const name = /^[a-zA-Z0-9_-]*/.exec(text.slice(i + 1))![0]; const end = i + 1 + name.length; const next = text[end]; if ((name || partial) && (next === undefined || !/[$/\\]/.test(next))) { mentions.push({ name, start: i, end }); } i = end; continue; } i++; } return mentions; } export function completionPrefix(lines: string[], line: number, col: number): string | undefined { const text = [...lines.slice(0, line), (lines[line] ?? "").slice(0, col)].join("\n"); const last = findMentions(text, true).at(-1); return last?.end === text.length ? `$${last.name}` : undefined; }