fix(lsp): Find usages in unopened files outside the language server's scope (#89)

Reference-based features only asked the language server, which analyses its configured scope (e.g. Pyright's `include`) plus open files, so usages in unopened files outside that scope were silently missing.

Find Usages, Show Usages, usage counts, Safe Delete of a symbol, Rename and incoming calls now also search project files that contain the symbol's name (whole word, same language, .gitignore/excludes respected, unsaved edits included, up to 300 files, cancellable). Candidates the first answer didn't cover are opened in the server temporarily, queried and closed again. Files the user has open are never touched, and diagnostics for the temporary files are dropped. An empty result while the server is still indexing is retried once indexing ends. When results may still be incomplete (indexing, file cap), the usages UI says so. `lsp_references` takes a RequestId and is cancelled by newer searches. Not covered yet: the Safe Delete scan when deleting a file or directory.

Fixes #83

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-27 17:41:20 +08:00
1 джерело 482f391fd1
коміт 8ec35c94ec
17 files changed
+1116 -97

No files matched your search

+4 -2
Переглянути файл
@@ -9,7 +9,7 @@ import { EditorSelection, MapMode, StateEffect, StateField, type Extension, type
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
import { codeLenses, type CodeLens } from "@/editor/api";
import { executeAction } from "@/platform/actions";
import { commands, type OutlineItem, type SymbolKind } from "@/platform/ipc";
import { commands, nextRequestId, type OutlineItem, type SymbolKind } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { debounce, docOf, isUnavailable, lspRequest } from "./lsp";
import { posToOffset } from "./positions";
@@ -166,7 +166,9 @@ class UsagesLensPlugin {
await Promise.all(
todo.slice(i, i + CONCURRENCY).map(async (t) => {
try {
const refs = await lspRequest(() => commands.lspReferences(doc.doc, { line: t.line, col: t.col }, false));
const { locations: refs } = await lspRequest(() =>
commands.lspReferences(nextRequestId(), doc.doc, { line: t.line, col: t.col }, false, true),
);
this.cache.set(key(t), refs.length);
} catch (e) {
if (isUnavailable(e)) unavailable = true;
+4 -1
Переглянути файл
@@ -16,7 +16,10 @@ let outline: Answer<OutlineItem[]> = ok([]);
let references: Answer<Location[]> = ok([]);
const lspGoto = vi.fn(async () => goto());
const docOutline = vi.fn(async () => outline());
const lspReferences = vi.fn(async () => references());
const lspReferences = vi.fn(async () => {
const r = references();
return r.status === "ok" ? { status: "ok" as const, data: { locations: r.data, incomplete: null } } : r;
});
const lspCodeActions = vi.fn(async () => ({ status: "ok", data: [] }));
const lspApplyCodeAction = vi.fn(async () => ({ status: "ok", data: null }));
const docOpen = vi.fn(async (path: string) => ({ status: "ok", data: { id: 99, path } }));
+4 -3
Переглянути файл
@@ -13,7 +13,7 @@
import type { EditorView } from "@codemirror/view";
import type { Text } from "@codemirror/state";
import type { LocalSymbol, Location, OutlineItem, Pos, Range, SyntaxNode } from "@/bindings";
import { call, commands, isCancelled, isTauri } from "@/platform/ipc";
import { call, commands, isCancelled, isTauri, nextRequestId } from "@/platform/ipc";
import { executeAction } from "@/platform/actions";
import { basename } from "@/platform/project";
import { editorsFor, whenSynced, type EditorDocContext } from "@/editor/api";
@@ -252,7 +252,7 @@ async function findUsages(ctx: EditorDocContext, target: SafeDeleteTarget): Prom
comparePos(range.start, target.range.end) > 0;
if (target.local)
return target.local.references.filter((r) => outside(ctx.path, r)).map((range) => ({ path: ctx.path, range }));
const refs = await lspRequest(() => commands.lspReferences(ctx.doc, target.at, false));
const { locations: refs } = await lspRequest(() => commands.lspReferences(nextRequestId(), ctx.doc, target.at, false, true));
return refs.filter((l) => outside(l.path, l.range));
}
@@ -517,7 +517,8 @@ export async function fileUsages(paths: readonly string[], signal?: AbortSignal)
if (signal?.aborted) return;
let refs: Location[];
try {
refs = await lspRequest(() => commands.lspReferences(doc, item.selection_range.start, false));
// Every symbol of every file: the server's own scope (a project search each would be too slow).
refs = (await lspRequest(() => commands.lspReferences(nextRequestId(), doc, item.selection_range.start, false, false))).locations;
} catch (e) {
if (isUnavailable(e)) {
reason = errorText(e);