/** Language-server and plugin lenses in the shared editor code-vision renderer. */ import { MapMode, StateEffect, StateField, type Extension } from "@codemirror/state"; import { ViewPlugin, type ViewUpdate } from "@codemirror/view"; import { codeLenses, shownAgain, whenSynced, type CodeLens, type EditorDocContext } from "@/editor/api"; import { call, commands } from "@/platform/ipc"; import { onNavigationProvidersChanged } from "@/editor/navigationProviders"; import { posToOffset } from "./positions"; import { onLspRefresh, onServerSettled } from "./availability"; /** * A server's reference count ("3 references", csharp-ls's "3 Reference(s)"): Code Vision's * Usages shows the same count above the declaration, under its own setting. */ const REFERENCE_COUNT = /^\s*\d+\s+references?(\(s\))?\s*$/i; const set = StateEffect.define(); const field = StateField.define({ create: () => [], update(value, tr) { for (const e of tr.effects) if (e.is(set)) return e.value; // Map through edits so block widgets don't vanish/reappear and thrash scroll (#2710). if (!tr.docChanged || !value.length) return value; const out: CodeLens[] = []; for (const l of value) { const pos = tr.changes.mapPos(l.pos, -1, MapMode.TrackDel); if (pos !== null) out.push({ ...l, pos }); } return out; }, provide: (f) => codeLenses.from(f), }); export function serverCodeLenses(ctx: EditorDocContext): Extension { if (ctx.doc < 0) return []; return [ field, ViewPlugin.define( (view) => new (class { timer: ReturnType | undefined; generation = 0; skipped = false; off: () => void; constructor() { const offs = [ onNavigationProvidersChanged(() => this.schedule()), onLspRefresh("codeLens", () => this.schedule()), onServerSettled(() => this.schedule()), ]; this.off = () => offs.forEach((f) => f()); this.schedule(); } schedule() { this.generation++; clearTimeout(this.timer); this.timer = setTimeout(() => void this.run(), 400); } update(u: ViewUpdate) { if (u.docChanged || (this.skipped && shownAgain(u))) this.schedule(); } async run() { const gen = this.generation; const text = view.state.doc; try { await whenSynced(view); const result = await call(commands.lspCodeLens(ctx.doc)); if (gen !== this.generation || text !== view.state.doc) return; this.skipped = !view.dom.isConnected; if (this.skipped) return; view.dispatch({ effects: set.of( result.flatMap((l) => l.command && !REFERENCE_COUNT.test(l.command.title) ? [ { pos: posToOffset(text, l.range.start), title: l.command.title, run: () => { void call(commands.lspExecuteCommand(ctx.doc, l.command!)).catch(console.warn); }, }, ] : [], ), ), }); } catch { if (gen === this.generation && view.dom.isConnected) view.dispatch({ effects: set.of([]) }); } } destroy() { this.generation++; clearTimeout(this.timer); this.off(); } })(), ), ]; }