// `#N` and `@user` in text the user is typing (spec §6.2): what is being // completed at the cursor, and which reference a position sits on. No // vscode imports, so it is tested with plain Node. /** A reference being typed: `#12` (issue or proposal) or `@an` (a user). */ export type Typing = { kind: "number"; prefix: string; start: number } | { kind: "user"; prefix: string; start: number }; /** * What the text before the cursor is in the middle of typing, if anything. * A reference starts a word: after the line start, whitespace or an * opening bracket, so `a#1` and `mail@host` are not references. */ export function typingAt(before: string): Typing | undefined { const n = /(^|[\s([{])#(\d*)$/.exec(before); if (n) { return { kind: "number", prefix: n[2], start: before.length - n[2].length - 1 }; } const u = /(^|[\s([{])@([A-Za-z0-9][A-Za-z0-9_-]*|)$/.exec(before); if (u) { return { kind: "user", prefix: u[2], start: before.length - u[2].length - 1 }; } return undefined; } /** The `#N` reference covering `column` of `line`, with its span. */ export function numberAt(line: string, column: number): { number: number; start: number; end: number } | undefined { const re = /(^|[\s([{])#(\d+)\b/g; for (let m = re.exec(line); m; m = re.exec(line)) { const start = m.index + m[1].length; const end = start + 1 + m[2].length; if (column >= start && column <= end) { return { number: Number(m[2]), start, end }; } } return undefined; } /** A repository root named by an SCM input box's URI query (`rootUri=file:///…`). */ export function rootFromScmQuery(query: string): string | undefined { const raw = new URLSearchParams(query).get("rootUri"); if (!raw) { return undefined; } try { return decodeURIComponent(new URL(raw).pathname); } catch { return undefined; } } /** The note of a `TODO`/`FIXME`/`XXX`/`HACK` comment line, as an issue title. */ export function todoTitle(line: string): string | undefined { const m = /\b(TODO|FIXME|XXX|HACK)\b(?:\([^)]*\))?[:\s-]*(.*)$/.exec(line); if (!m) { return undefined; } const text = m[2].replace(/\s*(\*\/|-->|#\}|\*\)|;)\s*$/, "").trim(); return text ? text : `${m[1]} in the code`; } /** The TODO line with the new issue's number recorded: `TODO(#12): …`. */ export function annotateTodo(line: string, number: number): string { return line.replace(/\b(TODO|FIXME|XXX|HACK)\b(?!\()/, `$1(#${number})`); } /** An issue body quoting the code, with a permalink to it on the hub. */ export function issueBody(opts: { permalink: string; path: string; startLine: number; endLine: number; language: string; code: string; pinned: boolean }): string { const where = opts.startLine === opts.endLine ? `line ${opts.startLine}` : `lines ${opts.startLine}–${opts.endLine}`; const fence = opts.code.includes("```") ? "~~~" : "```"; const note = opts.pinned ? "" : "\n\n_The link follows the bookmark, which has several heads, so the lines may move._"; return `From [\`${opts.path}\`, ${where}](${opts.permalink}):\n\n${fence}${opts.language}\n${opts.code.replace(/\n$/, "")}\n${fence}${note}\n`; }