fix(lsp): Find usages in files outside the server's analysis scope

Language servers answer textDocument/references (and rename, incoming
calls) only from the files they analyse: their configured scope (Pyright's
`include`, a tsconfig, a compilation database) plus the open documents.
Usages in other project files were silently missing until the file was
opened.

Reference searches now also find the project files of the server's
languages that mention the symbol's name as a whole word (honouring
.gitignore, excluded roots and ignored names; at most 300 files; off the
UI thread and cancellable). Those the first answer did not cover and the
user does not have open are opened on the server from disk, the request is
repeated, and they are closed again. Diagnostics the server publishes for
these files (including shortly after the close) never reach Problems, and
a user opening such a file mid-search takes it over cleanly.

An empty answer while the server reports progress is retried once
indexing ends; results that may still be incomplete (server still
indexing, or more matching files than searched) carry a note in Find
Usages and Show Usages. lsp_references takes a request id so searches are
cancelled when their tab closes, a new popup search starts, or the project
changes.

Find Usages, Show Usages (and Cmd-click on a declaration), usage counts,
Safe Delete of a symbol, Rename and incoming Call Hierarchy share the
path. File-level Safe Delete scans every symbol of the deleted files and
keeps to the server's scope.

Fixes #83

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
l0ng-ai committed 2026-10-27 17:40:07 +08:00
1 джерело 4100ab3341
коміт faf4d2bd36
17 files changed
+1116 -97

No files matched your search

+11 -2
Переглянути файл
@@ -106,16 +106,25 @@ pub async fn lsp_virtual_document(
project!(window, state).lsp.virtual_document(&uri).await
}
/// References of the symbol at `pos`. `project_wide` also searches project files
/// outside the language server's analysis scope; bulk scans over many symbols
/// leave it off. Cancel with `cancel_request(request)`.
#[tauri::command]
#[specta::specta]
pub async fn lsp_references(
window: Window,
state: State<'_, AppState>,
request: RequestId,
doc: DocId,
pos: Pos,
include_declaration: bool,
) -> IdeResult<Vec<Location>> {
project!(window, state).lsp.references(doc, pos, include_declaration).await
project_wide: bool,
) -> IdeResult<References> {
let lsp = project!(window, state).lsp.clone();
let cancel = state.begin_request(request);
let result = lsp.references(doc, pos, include_declaration, project_wide, cancel).await;
state.end_request(request);
result
}
#[tauri::command]
+33
Переглянути файл
@@ -442,6 +442,12 @@ impl AppState {
content_watchers: Mutex::new(content_watchers),
_scratch_watcher: scratch_watcher,
});
{
let weak = Arc::downgrade(&project);
project.lsp.set_candidates_hook(Box::new(move |q, cancel| {
weak.upgrade().map(|p| p.reference_candidates(q, cancel)).unwrap_or_default()
}));
}
self.projects.write().insert(window.to_owned(), project.clone());
Ok(project)
}
@@ -609,6 +615,33 @@ impl Project {
out
}
/// Project files of `q.languages` under `q.root` containing `q.word` as a whole
/// word (unsaved buffers included; excluded roots, ignored names and
/// `.gitignore`d files skipped): the files a references search opens on its server.
pub fn reference_candidates(
&self,
q: &ide_lsp::CandidateQuery,
cancel: &CancellationToken,
) -> ide_lsp::CandidateFiles {
if !q.root.starts_with(&self.root) {
return ide_lsp::CandidateFiles::default();
}
let query = ide_search::WordQuery {
word: q.word.clone(),
dir: q.root.clone(),
exclude: self.search_excludes(),
limit: q.limit,
};
let filter = |rel: &str| q.languages.contains(&self.languages.detect(&self.root.join(rel), None));
match ide_search::files_with_word(&self.root, &query, Some(&filter), &|p| self.documents.text_of(p), cancel) {
Ok(found) => ide_lsp::CandidateFiles { files: found.files, limit_hit: found.limit_hit },
Err(e) => {
tracing::debug!("reference candidates: {}", e.message);
ide_lsp::CandidateFiles::default()
}
}
}
/// `files.exclude`, the ignored names and the effective excluded roots (user-marked and detected).
pub fn exclude_globs(&self) -> Vec<String> {
exclude_globs(&self.settings.effective(), &self.detected_roots.read())