// `#N` and `@user` completion, and hover on `#N` (spec §6.2), in the // Oxygen commit box and in review comments. The numbers are the // repository's issues and proposals (one number space, as on the hub); // the users are the people who opened or are assigned to them. import * as vscode from "vscode"; import type * as api from "./hubapi"; import { HubAuth } from "./auth"; import type { HubRepo } from "./proposals"; import { numberAt, rootFromScmQuery, typingAt } from "./refs"; interface Ref { number: number; kind: "issue" | "proposal"; title: string; state: string; author: string; } interface Index { at: number; refs: Map; users: Set; } /** How long an index is reused before the hub is asked again. */ const FRESH_MS = 60_000; /** Where references are typed: the SCM commit box and comment editors. */ export const SELECTOR: vscode.DocumentSelector = [{ scheme: "vscode-scm" }, { scheme: "comment" }]; export class ReferenceIndex { private readonly cache = new Map(); constructor( private readonly auth: HubAuth, private readonly repos: () => Promise, ) {} clear(): void { this.cache.clear(); } /** The repository a document types into: the SCM box's root, else the only hub repository. */ async repoFor(doc: vscode.TextDocument): Promise { const repos = await this.repos(); const root = doc.uri.scheme === "vscode-scm" ? rootFromScmQuery(doc.uri.query) : undefined; if (root) { return repos.find((r) => r.handle.root === root); } return repos.length === 1 ? repos[0] : undefined; } async indexOf(repo: HubRepo): Promise { const key = `${repo.base}/${repo.owner}/${repo.name}`; const hit = this.cache.get(key); if (hit && Date.now() - hit.at < FRESH_MS) { return hit; } const client = this.auth.clientFor(repo.base); const [issues, proposals] = await Promise.all([client.issues(repo.owner, repo.name, "all").catch(() => [] as api.Issue[]), client.proposals(repo.owner, repo.name).catch(() => [] as api.Proposal[])]); const refs = new Map(); const users = new Set(); for (const i of issues) { refs.set(i.number, { number: i.number, kind: "issue", title: i.title, state: i.state, author: i.author }); users.add(i.author); i.assignees.forEach((a) => users.add(a)); } for (const p of proposals) { refs.set(p.number, { number: p.number, kind: "proposal", title: p.title, state: p.state, author: p.author }); users.add(p.author); } const me = this.auth.sessionFor(repo.base)?.username; if (me) { users.add(me); } users.delete(""); const index = { at: Date.now(), refs, users }; this.cache.set(key, index); return index; } } export class ReferenceCompletions implements vscode.CompletionItemProvider { constructor(private readonly index: ReferenceIndex) {} async provideCompletionItems(doc: vscode.TextDocument, pos: vscode.Position): Promise { const before = doc.lineAt(pos.line).text.slice(0, pos.character); const typing = typingAt(before); const repo = typing && (await this.index.repoFor(doc)); if (!typing || !repo) { return []; } const idx = await this.index.indexOf(repo); const range = new vscode.Range(pos.line, typing.start, pos.line, pos.character); if (typing.kind === "number") { // Open items first, then newest. const refs = [...idx.refs.values()].sort((a, b) => Number(a.state !== "open") - Number(b.state !== "open") || b.number - a.number); return refs.map((r, i) => { const item = new vscode.CompletionItem({ label: `#${r.number}`, description: r.title }, r.kind === "issue" ? vscode.CompletionItemKind.Issue : vscode.CompletionItemKind.Reference); item.detail = `${r.kind} · ${r.state} · ${r.author}`; item.filterText = `#${r.number} ${r.title}`; item.sortText = String(i).padStart(6, "0"); item.insertText = `#${r.number}`; item.range = range; return item; }); } return [...idx.users].sort().map((u) => { const item = new vscode.CompletionItem(`@${u}`, vscode.CompletionItemKind.User); item.insertText = `@${u}`; item.range = range; return item; }); } } export class ReferenceHovers implements vscode.HoverProvider { constructor(private readonly index: ReferenceIndex) {} async provideHover(doc: vscode.TextDocument, pos: vscode.Position): Promise { const at = numberAt(doc.lineAt(pos.line).text, pos.character); const repo = at && (await this.index.repoFor(doc)); if (!at || !repo) { return undefined; } const r = (await this.index.indexOf(repo)).refs.get(at.number); if (!r) { return undefined; } const url = `${repo.base}/${repo.owner}/${repo.name}/${r.kind}/${r.number}`; const md = new vscode.MarkdownString(`**${r.kind === "issue" ? "Issue" : "Proposal"} #${r.number}** · ${r.state}\n\n${r.title}\n\n_${r.author}_ · [open on the hub](${url})`); return new vscode.Hover(md, new vscode.Range(pos.line, at.start, pos.line, at.end)); } }