[ty] Rejigger workspace symbols for more efficient caching

In effect, we make the Salsa query aspect keyed only on whether we want
global symbols. We move everything else (hierarchical and querying) to
an aggregate step *after* the query.

This was a somewhat involved change since we want to return a flattened
list from visiting the source while also preserving enough information
to reform the symbols into a hierarchical structure that the LSP
expects. But I think overall the API has gotten simpler and we encode
more invariants into the type system. (For example, previously you got a
runtime assertion if you tried to provide a query string while enabling
hierarchical mode. But now that's prevented by construction.)
This commit is contained in:
Andrew Gallant 2025-08-23 08:11:39 -04:00 committed by Andrew Gallant
parent f407f12f4c
commit 205eae14d2
8 changed files with 317 additions and 164 deletions

View file

@ -1,29 +1,16 @@
use crate::symbols::{SymbolInfo, SymbolsOptions, symbols_for_file};
use crate::symbols::{FlatSymbols, symbols_for_file};
use ruff_db::files::File;
use ty_project::Db;
/// Get all document symbols for a file with the given options.
pub fn document_symbols_with_options(
db: &dyn Db,
file: File,
options: &SymbolsOptions,
) -> Vec<SymbolInfo> {
symbols_for_file(db, file, options).cloned().collect()
}
/// Get all document symbols for a file (hierarchical by default).
pub fn document_symbols(db: &dyn Db, file: File) -> Vec<SymbolInfo> {
let options = SymbolsOptions {
hierarchical: true,
global_only: false,
query_string: None,
};
document_symbols_with_options(db, file, &options)
pub fn document_symbols(db: &dyn Db, file: File) -> &FlatSymbols {
symbols_for_file(db, file)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbols::{HierarchicalSymbols, SymbolId, SymbolInfo};
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
@ -324,42 +311,45 @@ class OuterClass:
impl CursorTest {
fn document_symbols(&self) -> String {
let symbols = document_symbols(&self.db, self.cursor.file);
let symbols = document_symbols(&self.db, self.cursor.file).to_hierarchical();
if symbols.is_empty() {
return "No symbols found".to_string();
}
self.render_diagnostics(
symbols
.into_iter()
.flat_map(|symbol| symbol_to_diagnostics(symbol, self.cursor.file)),
)
self.render_diagnostics(symbols.iter().flat_map(|(id, symbol)| {
symbol_to_diagnostics(&symbols, id, symbol, self.cursor.file)
}))
}
}
fn symbol_to_diagnostics(symbol: SymbolInfo, file: File) -> Vec<DocumentSymbolDiagnostic> {
fn symbol_to_diagnostics<'db>(
symbols: &'db HierarchicalSymbols,
id: SymbolId,
symbol: SymbolInfo<'db>,
file: File,
) -> Vec<DocumentSymbolDiagnostic<'db>> {
// Output the symbol and recursively output all child symbols
let mut diagnostics = vec![DocumentSymbolDiagnostic::new(symbol.clone(), file)];
let mut diagnostics = vec![DocumentSymbolDiagnostic::new(symbol, file)];
for child in symbol.children {
diagnostics.extend(symbol_to_diagnostics(child, file));
for (child_id, child) in symbols.children(id) {
diagnostics.extend(symbol_to_diagnostics(symbols, child_id, child, file));
}
diagnostics
}
struct DocumentSymbolDiagnostic {
symbol: SymbolInfo,
struct DocumentSymbolDiagnostic<'db> {
symbol: SymbolInfo<'db>,
file: File,
}
impl DocumentSymbolDiagnostic {
fn new(symbol: SymbolInfo, file: File) -> Self {
impl<'db> DocumentSymbolDiagnostic<'db> {
fn new(symbol: SymbolInfo<'db>, file: File) -> Self {
Self { symbol, file }
}
}
impl IntoDiagnostic for DocumentSymbolDiagnostic {
impl IntoDiagnostic for DocumentSymbolDiagnostic<'_> {
fn into_diagnostic(self) -> Diagnostic {
let symbol_kind_str = self.symbol.kind.to_string();