Files
openidea/src/editor/lsp/serverCodeLens.ts
T
l0ng-ai fdc8bc2d4c fix(editor): leave reference counts to Code Vision's usages (#3139)
csharp-ls adds a "3 Reference(s)" lens above each declaration, next to Code Vision's own "3 usages". It duplicated the count, ignored the Code Vision settings, and was a second set of lens rows replaced after every edit. Language-server lenses that are only a reference count are now left out; Settings › Editor › Inlay Hints › Code Vision › Usages governs the count.

Refs #3107
2026-11-13 18:41:34 +08:00

98 lines
3.7 KiB
TypeScript

/** 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<CodeLens[]>();
const field = StateField.define<CodeLens[]>({
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<typeof setTimeout> | 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();
}
})(),
),
];
}