mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:51:25 +00:00
[ty] Added support for "document symbols" and "workspace symbols" (#19521)
This PR adds support for "document symbols" and "workspace symbols" language server features. Most of the logic to implement these features is shared. The "document symbols" feature returns a list of all symbols within a specified source file. Clients can specify whether they want a flat or hierarchical list. Document symbols are typically presented by a client in an "outline" form. Here's what this looks like in VS Code, for example. <img width="240" height="249" alt="image" src="https://github.com/user-attachments/assets/82b11f4f-32ec-4165-ba01-d6496ad13bdf" /> The "workspace symbols" feature returns a list of all symbols across the entire workspace that match some user-supplied query string. This allows the user to quickly find and navigate to any symbol within their code. <img width="450" height="134" alt="image" src="https://github.com/user-attachments/assets/aac131e0-9464-4adf-8a6c-829da028c759" /> --------- Co-authored-by: UnboundVariable <unbound@gmail.com>
This commit is contained in:
parent
165091a31c
commit
a0d8ff51dd
13 changed files with 1153 additions and 0 deletions
383
crates/ty_ide/src/document_symbols.rs
Normal file
383
crates/ty_ide/src/document_symbols.rs
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
use crate::symbols::{SymbolInfo, SymbolsOptions, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use ruff_db::diagnostic::{
|
||||||
|
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||||
|
SubDiagnosticSeverity,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_document_symbols_simple() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"
|
||||||
|
def hello():
|
||||||
|
pass
|
||||||
|
|
||||||
|
class World:
|
||||||
|
def method(self):
|
||||||
|
pass
|
||||||
|
<CURSOR>",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.document_symbols(), @r"
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:2:5
|
||||||
|
|
|
||||||
|
2 | def hello():
|
||||||
|
| ^^^^^
|
||||||
|
3 | pass
|
||||||
|
|
|
||||||
|
info: Function hello
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:5:7
|
||||||
|
|
|
||||||
|
3 | pass
|
||||||
|
4 |
|
||||||
|
5 | class World:
|
||||||
|
| ^^^^^
|
||||||
|
6 | def method(self):
|
||||||
|
7 | pass
|
||||||
|
|
|
||||||
|
info: Class World
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:6:9
|
||||||
|
|
|
||||||
|
5 | class World:
|
||||||
|
6 | def method(self):
|
||||||
|
| ^^^^^^
|
||||||
|
7 | pass
|
||||||
|
|
|
||||||
|
info: Method method
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_document_symbols_complex() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"
|
||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
CONSTANT = 42
|
||||||
|
variable = 'hello'
|
||||||
|
typed_global: str = 'typed'
|
||||||
|
annotated_only: int
|
||||||
|
|
||||||
|
class MyClass:
|
||||||
|
class_var = 100
|
||||||
|
typed_class_var: str = 'class_typed'
|
||||||
|
annotated_class_var: float
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.instance_var = 0
|
||||||
|
|
||||||
|
def public_method(self):
|
||||||
|
return self.instance_var
|
||||||
|
|
||||||
|
def _private_method(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def standalone_function():
|
||||||
|
local_var = 10
|
||||||
|
return local_var
|
||||||
|
<CURSOR>",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.document_symbols(), @r"
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:5:1
|
||||||
|
|
|
||||||
|
3 | from typing import List
|
||||||
|
4 |
|
||||||
|
5 | CONSTANT = 42
|
||||||
|
| ^^^^^^^^
|
||||||
|
6 | variable = 'hello'
|
||||||
|
7 | typed_global: str = 'typed'
|
||||||
|
|
|
||||||
|
info: Constant CONSTANT
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:6:1
|
||||||
|
|
|
||||||
|
5 | CONSTANT = 42
|
||||||
|
6 | variable = 'hello'
|
||||||
|
| ^^^^^^^^
|
||||||
|
7 | typed_global: str = 'typed'
|
||||||
|
8 | annotated_only: int
|
||||||
|
|
|
||||||
|
info: Variable variable
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:7:1
|
||||||
|
|
|
||||||
|
5 | CONSTANT = 42
|
||||||
|
6 | variable = 'hello'
|
||||||
|
7 | typed_global: str = 'typed'
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
8 | annotated_only: int
|
||||||
|
|
|
||||||
|
info: Variable typed_global
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:8:1
|
||||||
|
|
|
||||||
|
6 | variable = 'hello'
|
||||||
|
7 | typed_global: str = 'typed'
|
||||||
|
8 | annotated_only: int
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
9 |
|
||||||
|
10 | class MyClass:
|
||||||
|
|
|
||||||
|
info: Variable annotated_only
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:10:7
|
||||||
|
|
|
||||||
|
8 | annotated_only: int
|
||||||
|
9 |
|
||||||
|
10 | class MyClass:
|
||||||
|
| ^^^^^^^
|
||||||
|
11 | class_var = 100
|
||||||
|
12 | typed_class_var: str = 'class_typed'
|
||||||
|
|
|
||||||
|
info: Class MyClass
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:11:5
|
||||||
|
|
|
||||||
|
10 | class MyClass:
|
||||||
|
11 | class_var = 100
|
||||||
|
| ^^^^^^^^^
|
||||||
|
12 | typed_class_var: str = 'class_typed'
|
||||||
|
13 | annotated_class_var: float
|
||||||
|
|
|
||||||
|
info: Field class_var
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:12:5
|
||||||
|
|
|
||||||
|
10 | class MyClass:
|
||||||
|
11 | class_var = 100
|
||||||
|
12 | typed_class_var: str = 'class_typed'
|
||||||
|
| ^^^^^^^^^^^^^^^
|
||||||
|
13 | annotated_class_var: float
|
||||||
|
|
|
||||||
|
info: Field typed_class_var
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:13:5
|
||||||
|
|
|
||||||
|
11 | class_var = 100
|
||||||
|
12 | typed_class_var: str = 'class_typed'
|
||||||
|
13 | annotated_class_var: float
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
14 |
|
||||||
|
15 | def __init__(self):
|
||||||
|
|
|
||||||
|
info: Field annotated_class_var
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:15:9
|
||||||
|
|
|
||||||
|
13 | annotated_class_var: float
|
||||||
|
14 |
|
||||||
|
15 | def __init__(self):
|
||||||
|
| ^^^^^^^^
|
||||||
|
16 | self.instance_var = 0
|
||||||
|
|
|
||||||
|
info: Constructor __init__
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:18:9
|
||||||
|
|
|
||||||
|
16 | self.instance_var = 0
|
||||||
|
17 |
|
||||||
|
18 | def public_method(self):
|
||||||
|
| ^^^^^^^^^^^^^
|
||||||
|
19 | return self.instance_var
|
||||||
|
|
|
||||||
|
info: Method public_method
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:21:9
|
||||||
|
|
|
||||||
|
19 | return self.instance_var
|
||||||
|
20 |
|
||||||
|
21 | def _private_method(self):
|
||||||
|
| ^^^^^^^^^^^^^^^
|
||||||
|
22 | pass
|
||||||
|
|
|
||||||
|
info: Method _private_method
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:24:5
|
||||||
|
|
|
||||||
|
22 | pass
|
||||||
|
23 |
|
||||||
|
24 | def standalone_function():
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
25 | local_var = 10
|
||||||
|
26 | return local_var
|
||||||
|
|
|
||||||
|
info: Function standalone_function
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_document_symbols_nested() {
|
||||||
|
let test = cursor_test(
|
||||||
|
"
|
||||||
|
class OuterClass:
|
||||||
|
OUTER_CONSTANT = 100
|
||||||
|
|
||||||
|
def outer_method(self):
|
||||||
|
return self.OUTER_CONSTANT
|
||||||
|
|
||||||
|
class InnerClass:
|
||||||
|
def inner_method(self):
|
||||||
|
pass
|
||||||
|
<CURSOR>",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.document_symbols(), @r"
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:2:7
|
||||||
|
|
|
||||||
|
2 | class OuterClass:
|
||||||
|
| ^^^^^^^^^^
|
||||||
|
3 | OUTER_CONSTANT = 100
|
||||||
|
|
|
||||||
|
info: Class OuterClass
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:3:5
|
||||||
|
|
|
||||||
|
2 | class OuterClass:
|
||||||
|
3 | OUTER_CONSTANT = 100
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
4 |
|
||||||
|
5 | def outer_method(self):
|
||||||
|
|
|
||||||
|
info: Constant OUTER_CONSTANT
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:5:9
|
||||||
|
|
|
||||||
|
3 | OUTER_CONSTANT = 100
|
||||||
|
4 |
|
||||||
|
5 | def outer_method(self):
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
6 | return self.OUTER_CONSTANT
|
||||||
|
|
|
||||||
|
info: Method outer_method
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:8:11
|
||||||
|
|
|
||||||
|
6 | return self.OUTER_CONSTANT
|
||||||
|
7 |
|
||||||
|
8 | class InnerClass:
|
||||||
|
| ^^^^^^^^^^
|
||||||
|
9 | def inner_method(self):
|
||||||
|
10 | pass
|
||||||
|
|
|
||||||
|
info: Class InnerClass
|
||||||
|
|
||||||
|
info[document-symbols]: SymbolInfo
|
||||||
|
--> main.py:9:13
|
||||||
|
|
|
||||||
|
8 | class InnerClass:
|
||||||
|
9 | def inner_method(self):
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
10 | pass
|
||||||
|
|
|
||||||
|
info: Method inner_method
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorTest {
|
||||||
|
fn document_symbols(&self) -> String {
|
||||||
|
let symbols = document_symbols(&self.db, self.cursor.file);
|
||||||
|
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol_to_diagnostics(symbol: SymbolInfo, file: File) -> Vec<DocumentSymbolDiagnostic> {
|
||||||
|
// Output the symbol and recursively output all child symbols
|
||||||
|
let mut diagnostics = vec![DocumentSymbolDiagnostic::new(symbol.clone(), file)];
|
||||||
|
|
||||||
|
for child in symbol.children {
|
||||||
|
diagnostics.extend(symbol_to_diagnostics(child, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
struct DocumentSymbolDiagnostic {
|
||||||
|
symbol: SymbolInfo,
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentSymbolDiagnostic {
|
||||||
|
fn new(symbol: SymbolInfo, file: File) -> Self {
|
||||||
|
Self { symbol, file }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDiagnostic for DocumentSymbolDiagnostic {
|
||||||
|
fn into_diagnostic(self) -> Diagnostic {
|
||||||
|
let symbol_kind_str = self.symbol.kind.to_string();
|
||||||
|
|
||||||
|
let info_text = format!("{} {}", symbol_kind_str, self.symbol.name);
|
||||||
|
|
||||||
|
let sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, info_text);
|
||||||
|
|
||||||
|
let mut main = Diagnostic::new(
|
||||||
|
DiagnosticId::Lint(LintName::of("document-symbols")),
|
||||||
|
Severity::Info,
|
||||||
|
"SymbolInfo".to_string(),
|
||||||
|
);
|
||||||
|
main.annotate(Annotation::primary(
|
||||||
|
Span::from(self.file).with_range(self.symbol.name_range),
|
||||||
|
));
|
||||||
|
main.sub(sub);
|
||||||
|
|
||||||
|
main
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
mod completion;
|
mod completion;
|
||||||
mod doc_highlights;
|
mod doc_highlights;
|
||||||
mod docstring;
|
mod docstring;
|
||||||
|
mod document_symbols;
|
||||||
mod find_node;
|
mod find_node;
|
||||||
mod goto;
|
mod goto;
|
||||||
mod goto_declaration;
|
mod goto_declaration;
|
||||||
|
@ -14,10 +15,13 @@ mod references;
|
||||||
mod semantic_tokens;
|
mod semantic_tokens;
|
||||||
mod signature_help;
|
mod signature_help;
|
||||||
mod stub_mapping;
|
mod stub_mapping;
|
||||||
|
mod symbols;
|
||||||
|
mod workspace_symbols;
|
||||||
|
|
||||||
pub use completion::completion;
|
pub use completion::completion;
|
||||||
pub use doc_highlights::document_highlights;
|
pub use doc_highlights::document_highlights;
|
||||||
pub use docstring::get_parameter_documentation;
|
pub use docstring::get_parameter_documentation;
|
||||||
|
pub use document_symbols::{document_symbols, document_symbols_with_options};
|
||||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||||
pub use goto_references::goto_references;
|
pub use goto_references::goto_references;
|
||||||
pub use hover::hover;
|
pub use hover::hover;
|
||||||
|
@ -28,6 +32,8 @@ pub use semantic_tokens::{
|
||||||
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
|
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
|
||||||
};
|
};
|
||||||
pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, signature_help};
|
pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, signature_help};
|
||||||
|
pub use symbols::{SymbolInfo, SymbolKind, SymbolsOptions};
|
||||||
|
pub use workspace_symbols::{WorkspaceSymbolInfo, workspace_symbols};
|
||||||
|
|
||||||
use ruff_db::files::{File, FileRange};
|
use ruff_db::files::{File, FileRange};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
309
crates/ty_ide/src/symbols.rs
Normal file
309
crates/ty_ide/src/symbols.rs
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
//! Implements logic used by the document symbol provider, workspace symbol
|
||||||
|
//! provider, and auto-import feature of the completion provider.
|
||||||
|
|
||||||
|
use ruff_db::files::File;
|
||||||
|
use ruff_db::parsed::parsed_module;
|
||||||
|
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor};
|
||||||
|
use ruff_python_ast::{Expr, Stmt};
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
use ty_project::Db;
|
||||||
|
|
||||||
|
/// Options that control which symbols are returned
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SymbolsOptions {
|
||||||
|
/// Return a hierarchy of symbols or a flattened list?
|
||||||
|
pub hierarchical: bool,
|
||||||
|
/// Include only symbols in the global scope
|
||||||
|
pub global_only: bool,
|
||||||
|
/// Query string for filtering symbol names
|
||||||
|
pub query_string: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Symbol information for IDE features like document outline.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SymbolInfo {
|
||||||
|
/// The name of the symbol
|
||||||
|
pub name: String,
|
||||||
|
/// The kind of symbol (function, class, variable, etc.)
|
||||||
|
pub kind: SymbolKind,
|
||||||
|
/// The range of the symbol name
|
||||||
|
pub name_range: TextRange,
|
||||||
|
/// The full range of the symbol (including body)
|
||||||
|
pub full_range: TextRange,
|
||||||
|
/// Child symbols (e.g., methods in a class)
|
||||||
|
pub children: Vec<SymbolInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of symbol
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SymbolKind {
|
||||||
|
Module,
|
||||||
|
Class,
|
||||||
|
Method,
|
||||||
|
Function,
|
||||||
|
Variable,
|
||||||
|
Constant,
|
||||||
|
Property,
|
||||||
|
Field,
|
||||||
|
Constructor,
|
||||||
|
Parameter,
|
||||||
|
TypeParameter,
|
||||||
|
Import,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolKind {
|
||||||
|
/// Returns the string representation of the symbol kind.
|
||||||
|
pub fn to_string(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SymbolKind::Module => "Module",
|
||||||
|
SymbolKind::Class => "Class",
|
||||||
|
SymbolKind::Method => "Method",
|
||||||
|
SymbolKind::Function => "Function",
|
||||||
|
SymbolKind::Variable => "Variable",
|
||||||
|
SymbolKind::Constant => "Constant",
|
||||||
|
SymbolKind::Property => "Property",
|
||||||
|
SymbolKind::Field => "Field",
|
||||||
|
SymbolKind::Constructor => "Constructor",
|
||||||
|
SymbolKind::Parameter => "Parameter",
|
||||||
|
SymbolKind::TypeParameter => "TypeParameter",
|
||||||
|
SymbolKind::Import => "Import",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn symbols_for_file(
|
||||||
|
db: &dyn Db,
|
||||||
|
file: File,
|
||||||
|
options: &SymbolsOptions,
|
||||||
|
) -> Vec<SymbolInfo> {
|
||||||
|
assert!(
|
||||||
|
!options.hierarchical || options.query_string.is_none(),
|
||||||
|
"Cannot use hierarchical mode with a query string"
|
||||||
|
);
|
||||||
|
|
||||||
|
let parsed = parsed_module(db, file);
|
||||||
|
let module = parsed.load(db);
|
||||||
|
|
||||||
|
let mut visitor = SymbolVisitor::new(options);
|
||||||
|
visitor.visit_body(&module.syntax().body);
|
||||||
|
visitor.symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SymbolVisitor<'a> {
|
||||||
|
symbols: Vec<SymbolInfo>,
|
||||||
|
symbol_stack: Vec<SymbolInfo>,
|
||||||
|
/// Track if we're currently inside a function (to exclude local variables)
|
||||||
|
in_function: bool,
|
||||||
|
/// Options controlling symbol collection
|
||||||
|
options: &'a SymbolsOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SymbolVisitor<'a> {
|
||||||
|
fn new(options: &'a SymbolsOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
symbols: Vec::new(),
|
||||||
|
symbol_stack: Vec::new(),
|
||||||
|
in_function: false,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_body(&mut self, body: &[Stmt]) {
|
||||||
|
for stmt in body {
|
||||||
|
self.visit_stmt(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_symbol(&mut self, symbol: SymbolInfo) {
|
||||||
|
// Filter by query string if provided
|
||||||
|
if let Some(ref query) = self.options.query_string {
|
||||||
|
if !Self::is_pattern_in_symbol(query, &symbol.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.options.hierarchical {
|
||||||
|
if let Some(parent) = self.symbol_stack.last_mut() {
|
||||||
|
parent.children.push(symbol);
|
||||||
|
} else {
|
||||||
|
self.symbols.push(symbol);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.symbols.push(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_symbol(&mut self, symbol: SymbolInfo) {
|
||||||
|
if self.options.hierarchical {
|
||||||
|
self.symbol_stack.push(symbol);
|
||||||
|
} else {
|
||||||
|
self.add_symbol(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_symbol(&mut self) {
|
||||||
|
if self.options.hierarchical {
|
||||||
|
if let Some(symbol) = self.symbol_stack.pop() {
|
||||||
|
self.add_symbol(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_constant_name(name: &str) -> bool {
|
||||||
|
name.chars().all(|c| c.is_ascii_uppercase() || c == '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if symbol name contains all characters in the query
|
||||||
|
/// string in order. The comparison is case insensitive.
|
||||||
|
fn is_pattern_in_symbol(query_string: &str, symbol_name: &str) -> bool {
|
||||||
|
let typed_lower = query_string.to_lowercase();
|
||||||
|
let symbol_lower = symbol_name.to_lowercase();
|
||||||
|
let typed_chars: Vec<char> = typed_lower.chars().collect();
|
||||||
|
let symbol_chars: Vec<char> = symbol_lower.chars().collect();
|
||||||
|
|
||||||
|
let mut typed_pos = 0;
|
||||||
|
let mut symbol_pos = 0;
|
||||||
|
|
||||||
|
while typed_pos < typed_chars.len() && symbol_pos < symbol_chars.len() {
|
||||||
|
if typed_chars[typed_pos] == symbol_chars[symbol_pos] {
|
||||||
|
typed_pos += 1;
|
||||||
|
}
|
||||||
|
symbol_pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
typed_pos == typed_chars.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceOrderVisitor<'_> for SymbolVisitor<'_> {
|
||||||
|
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||||
|
match stmt {
|
||||||
|
Stmt::FunctionDef(func_def) => {
|
||||||
|
let kind = if self
|
||||||
|
.symbol_stack
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.kind == SymbolKind::Class)
|
||||||
|
{
|
||||||
|
if func_def.name.as_str() == "__init__" {
|
||||||
|
SymbolKind::Constructor
|
||||||
|
} else {
|
||||||
|
SymbolKind::Method
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SymbolKind::Function
|
||||||
|
};
|
||||||
|
|
||||||
|
let symbol = SymbolInfo {
|
||||||
|
name: func_def.name.to_string(),
|
||||||
|
kind,
|
||||||
|
name_range: func_def.name.range(),
|
||||||
|
full_range: stmt.range(),
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.options.global_only {
|
||||||
|
self.add_symbol(symbol);
|
||||||
|
// If global_only, don't walk function bodies
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push_symbol(symbol);
|
||||||
|
|
||||||
|
// Mark that we're entering a function scope
|
||||||
|
let was_in_function = self.in_function;
|
||||||
|
self.in_function = true;
|
||||||
|
|
||||||
|
source_order::walk_stmt(self, stmt);
|
||||||
|
|
||||||
|
// Restore the previous function scope state
|
||||||
|
self.in_function = was_in_function;
|
||||||
|
|
||||||
|
self.pop_symbol();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stmt::ClassDef(class_def) => {
|
||||||
|
let symbol = SymbolInfo {
|
||||||
|
name: class_def.name.to_string(),
|
||||||
|
kind: SymbolKind::Class,
|
||||||
|
name_range: class_def.name.range(),
|
||||||
|
full_range: stmt.range(),
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.options.global_only {
|
||||||
|
self.add_symbol(symbol);
|
||||||
|
// If global_only, don't walk class bodies
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push_symbol(symbol);
|
||||||
|
source_order::walk_stmt(self, stmt);
|
||||||
|
self.pop_symbol();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stmt::Assign(assign) => {
|
||||||
|
// Include assignments only when we're in global or class scope
|
||||||
|
if !self.in_function {
|
||||||
|
for target in &assign.targets {
|
||||||
|
if let Expr::Name(name) = target {
|
||||||
|
let kind = if Self::is_constant_name(name.id.as_str()) {
|
||||||
|
SymbolKind::Constant
|
||||||
|
} else if self
|
||||||
|
.symbol_stack
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.kind == SymbolKind::Class)
|
||||||
|
{
|
||||||
|
SymbolKind::Field
|
||||||
|
} else {
|
||||||
|
SymbolKind::Variable
|
||||||
|
};
|
||||||
|
|
||||||
|
let symbol = SymbolInfo {
|
||||||
|
name: name.id.to_string(),
|
||||||
|
kind,
|
||||||
|
name_range: name.range(),
|
||||||
|
full_range: stmt.range(),
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_symbol(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stmt::AnnAssign(ann_assign) => {
|
||||||
|
// Include assignments only when we're in global or class scope
|
||||||
|
if !self.in_function {
|
||||||
|
if let Expr::Name(name) = &*ann_assign.target {
|
||||||
|
let kind = if Self::is_constant_name(name.id.as_str()) {
|
||||||
|
SymbolKind::Constant
|
||||||
|
} else if self
|
||||||
|
.symbol_stack
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.kind == SymbolKind::Class)
|
||||||
|
{
|
||||||
|
SymbolKind::Field
|
||||||
|
} else {
|
||||||
|
SymbolKind::Variable
|
||||||
|
};
|
||||||
|
|
||||||
|
let symbol = SymbolInfo {
|
||||||
|
name: name.id.to_string(),
|
||||||
|
kind,
|
||||||
|
name_range: name.range(),
|
||||||
|
full_range: stmt.range(),
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_symbol(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
source_order::walk_stmt(self, stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
crates/ty_ide/src/workspace_symbols.rs
Normal file
166
crates/ty_ide/src/workspace_symbols.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
use crate::symbols::{SymbolInfo, SymbolsOptions, symbols_for_file};
|
||||||
|
use ruff_db::files::File;
|
||||||
|
use ty_project::Db;
|
||||||
|
|
||||||
|
/// Get all workspace symbols matching the query string.
|
||||||
|
/// Returns symbols from all files in the workspace, filtered by the query.
|
||||||
|
pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec<WorkspaceSymbolInfo> {
|
||||||
|
// If the query is empty, return immediately to avoid expensive file scanning
|
||||||
|
if query.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let project = db.project();
|
||||||
|
|
||||||
|
let options = SymbolsOptions {
|
||||||
|
hierarchical: false, // Workspace symbols are always flat
|
||||||
|
global_only: false,
|
||||||
|
query_string: Some(query.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all files in the project
|
||||||
|
let files = project.files(db);
|
||||||
|
|
||||||
|
// For each file, extract symbols and add them to results
|
||||||
|
for file in files.iter() {
|
||||||
|
let file_symbols = symbols_for_file(db, *file, &options);
|
||||||
|
|
||||||
|
for symbol in file_symbols {
|
||||||
|
results.push(WorkspaceSymbolInfo {
|
||||||
|
symbol,
|
||||||
|
file: *file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A symbol found in the workspace, including the file it was found in.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct WorkspaceSymbolInfo {
|
||||||
|
/// The symbol information
|
||||||
|
pub symbol: SymbolInfo,
|
||||||
|
/// The file containing the symbol
|
||||||
|
pub file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::tests::CursorTest;
|
||||||
|
use crate::tests::IntoDiagnostic;
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use ruff_db::diagnostic::{
|
||||||
|
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||||
|
SubDiagnosticSeverity,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_symbols_multi_file() {
|
||||||
|
let test = CursorTest::builder()
|
||||||
|
.source(
|
||||||
|
"utils.py",
|
||||||
|
"
|
||||||
|
def utility_function():
|
||||||
|
'''A helpful utility function'''
|
||||||
|
pass
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.source(
|
||||||
|
"models.py",
|
||||||
|
"
|
||||||
|
class DataModel:
|
||||||
|
'''A data model class'''
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.source(
|
||||||
|
"constants.py",
|
||||||
|
"
|
||||||
|
API_BASE_URL = 'https://api.example.com'
|
||||||
|
<CURSOR>",
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_snapshot!(test.workspace_symbols("ufunc"), @r"
|
||||||
|
info[workspace-symbols]: WorkspaceSymbolInfo
|
||||||
|
--> utils.py:2:5
|
||||||
|
|
|
||||||
|
2 | def utility_function():
|
||||||
|
| ^^^^^^^^^^^^^^^^
|
||||||
|
3 | '''A helpful utility function'''
|
||||||
|
4 | pass
|
||||||
|
|
|
||||||
|
info: Function utility_function
|
||||||
|
");
|
||||||
|
|
||||||
|
assert_snapshot!(test.workspace_symbols("data"), @r"
|
||||||
|
info[workspace-symbols]: WorkspaceSymbolInfo
|
||||||
|
--> models.py:2:7
|
||||||
|
|
|
||||||
|
2 | class DataModel:
|
||||||
|
| ^^^^^^^^^
|
||||||
|
3 | '''A data model class'''
|
||||||
|
4 | def __init__(self):
|
||||||
|
|
|
||||||
|
info: Class DataModel
|
||||||
|
");
|
||||||
|
|
||||||
|
assert_snapshot!(test.workspace_symbols("apibase"), @r"
|
||||||
|
info[workspace-symbols]: WorkspaceSymbolInfo
|
||||||
|
--> constants.py:2:1
|
||||||
|
|
|
||||||
|
2 | API_BASE_URL = 'https://api.example.com'
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
info: Constant API_BASE_URL
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorTest {
|
||||||
|
fn workspace_symbols(&self, query: &str) -> String {
|
||||||
|
let symbols = workspace_symbols(&self.db, query);
|
||||||
|
|
||||||
|
if symbols.is_empty() {
|
||||||
|
return "No symbols found".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render_diagnostics(symbols.into_iter().map(WorkspaceSymbolDiagnostic::new))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WorkspaceSymbolDiagnostic {
|
||||||
|
symbol_info: WorkspaceSymbolInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceSymbolDiagnostic {
|
||||||
|
fn new(symbol_info: WorkspaceSymbolInfo) -> Self {
|
||||||
|
Self { symbol_info }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDiagnostic for WorkspaceSymbolDiagnostic {
|
||||||
|
fn into_diagnostic(self) -> Diagnostic {
|
||||||
|
let symbol_kind_str = self.symbol_info.symbol.kind.to_string();
|
||||||
|
|
||||||
|
let info_text = format!("{} {}", symbol_kind_str, self.symbol_info.symbol.name);
|
||||||
|
|
||||||
|
let sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, info_text);
|
||||||
|
|
||||||
|
let mut main = Diagnostic::new(
|
||||||
|
DiagnosticId::Lint(LintName::of("workspace-symbols")),
|
||||||
|
Severity::Info,
|
||||||
|
"WorkspaceSymbolInfo".to_string(),
|
||||||
|
);
|
||||||
|
main.annotate(Annotation::primary(
|
||||||
|
Span::from(self.symbol_info.file).with_range(self.symbol_info.symbol.name_range),
|
||||||
|
));
|
||||||
|
main.sub(sub);
|
||||||
|
|
||||||
|
main
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -241,6 +241,8 @@ impl Server {
|
||||||
trigger_characters: Some(vec!['.'.to_string()]),
|
trigger_characters: Some(vec!['.'.to_string()]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
document_symbol_provider: Some(lsp_types::OneOf::Left(true)),
|
||||||
|
workspace_symbol_provider: Some(lsp_types::OneOf::Left(true)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ mod diagnostics;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod requests;
|
mod requests;
|
||||||
mod semantic_tokens;
|
mod semantic_tokens;
|
||||||
|
mod symbols;
|
||||||
mod traits;
|
mod traits;
|
||||||
|
|
||||||
use self::traits::{NotificationHandler, RequestHandler};
|
use self::traits::{NotificationHandler, RequestHandler};
|
||||||
|
@ -83,6 +84,14 @@ pub(super) fn request(req: server::Request) -> Task {
|
||||||
>(
|
>(
|
||||||
req, BackgroundSchedule::LatencySensitive
|
req, BackgroundSchedule::LatencySensitive
|
||||||
),
|
),
|
||||||
|
requests::DocumentSymbolRequestHandler::METHOD => background_document_request_task::<
|
||||||
|
requests::DocumentSymbolRequestHandler,
|
||||||
|
>(req, BackgroundSchedule::Worker),
|
||||||
|
requests::WorkspaceSymbolRequestHandler::METHOD => background_request_task::<
|
||||||
|
requests::WorkspaceSymbolRequestHandler,
|
||||||
|
>(
|
||||||
|
req, BackgroundSchedule::Worker
|
||||||
|
),
|
||||||
lsp_types::request::Shutdown::METHOD => sync_request_task::<requests::ShutdownHandler>(req),
|
lsp_types::request::Shutdown::METHOD => sync_request_task::<requests::ShutdownHandler>(req),
|
||||||
|
|
||||||
method => {
|
method => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod completion;
|
mod completion;
|
||||||
mod diagnostic;
|
mod diagnostic;
|
||||||
mod doc_highlights;
|
mod doc_highlights;
|
||||||
|
mod document_symbols;
|
||||||
mod goto_declaration;
|
mod goto_declaration;
|
||||||
mod goto_definition;
|
mod goto_definition;
|
||||||
mod goto_references;
|
mod goto_references;
|
||||||
|
@ -12,10 +13,12 @@ mod semantic_tokens_range;
|
||||||
mod shutdown;
|
mod shutdown;
|
||||||
mod signature_help;
|
mod signature_help;
|
||||||
mod workspace_diagnostic;
|
mod workspace_diagnostic;
|
||||||
|
mod workspace_symbols;
|
||||||
|
|
||||||
pub(super) use completion::CompletionRequestHandler;
|
pub(super) use completion::CompletionRequestHandler;
|
||||||
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
||||||
pub(super) use doc_highlights::DocumentHighlightRequestHandler;
|
pub(super) use doc_highlights::DocumentHighlightRequestHandler;
|
||||||
|
pub(super) use document_symbols::DocumentSymbolRequestHandler;
|
||||||
pub(super) use goto_declaration::GotoDeclarationRequestHandler;
|
pub(super) use goto_declaration::GotoDeclarationRequestHandler;
|
||||||
pub(super) use goto_definition::GotoDefinitionRequestHandler;
|
pub(super) use goto_definition::GotoDefinitionRequestHandler;
|
||||||
pub(super) use goto_references::ReferencesRequestHandler;
|
pub(super) use goto_references::ReferencesRequestHandler;
|
||||||
|
@ -27,3 +30,4 @@ pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler;
|
||||||
pub(super) use shutdown::ShutdownHandler;
|
pub(super) use shutdown::ShutdownHandler;
|
||||||
pub(super) use signature_help::SignatureHelpRequestHandler;
|
pub(super) use signature_help::SignatureHelpRequestHandler;
|
||||||
pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler;
|
pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler;
|
||||||
|
pub(super) use workspace_symbols::WorkspaceSymbolRequestHandler;
|
||||||
|
|
125
crates/ty_server/src/server/api/requests/document_symbols.rs
Normal file
125
crates/ty_server/src/server/api/requests/document_symbols.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use lsp_types::request::DocumentSymbolRequest;
|
||||||
|
use lsp_types::{DocumentSymbol, DocumentSymbolParams, SymbolInformation, Url};
|
||||||
|
use ruff_db::source::{line_index, source_text};
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
|
use ty_ide::{SymbolInfo, SymbolsOptions, document_symbols_with_options};
|
||||||
|
use ty_project::ProjectDatabase;
|
||||||
|
|
||||||
|
use crate::document::{PositionEncoding, ToRangeExt};
|
||||||
|
use crate::server::api::symbols::{convert_symbol_kind, convert_to_lsp_symbol_information};
|
||||||
|
use crate::server::api::traits::{
|
||||||
|
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||||
|
};
|
||||||
|
use crate::session::DocumentSnapshot;
|
||||||
|
use crate::session::client::Client;
|
||||||
|
|
||||||
|
pub(crate) struct DocumentSymbolRequestHandler;
|
||||||
|
|
||||||
|
impl RequestHandler for DocumentSymbolRequestHandler {
|
||||||
|
type RequestType = DocumentSymbolRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
|
||||||
|
fn document_url(params: &DocumentSymbolParams) -> Cow<Url> {
|
||||||
|
Cow::Borrowed(¶ms.text_document.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_with_snapshot(
|
||||||
|
db: &ProjectDatabase,
|
||||||
|
snapshot: DocumentSnapshot,
|
||||||
|
_client: &Client,
|
||||||
|
params: DocumentSymbolParams,
|
||||||
|
) -> crate::server::Result<Option<lsp_types::DocumentSymbolResponse>> {
|
||||||
|
if snapshot.client_settings().is_language_services_disabled() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file) = snapshot.file(db) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = source_text(db, file);
|
||||||
|
let line_index = line_index(db, file);
|
||||||
|
|
||||||
|
// Check if the client supports hierarchical document symbols
|
||||||
|
let supports_hierarchical = snapshot
|
||||||
|
.resolved_client_capabilities()
|
||||||
|
.supports_hierarchical_document_symbols();
|
||||||
|
|
||||||
|
let options = SymbolsOptions {
|
||||||
|
hierarchical: supports_hierarchical,
|
||||||
|
global_only: false,
|
||||||
|
query_string: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let symbols = document_symbols_with_options(db, file, &options);
|
||||||
|
|
||||||
|
if symbols.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if supports_hierarchical {
|
||||||
|
// Return hierarchical symbols
|
||||||
|
let lsp_symbols: Vec<DocumentSymbol> = symbols
|
||||||
|
.into_iter()
|
||||||
|
.map(|symbol| {
|
||||||
|
convert_to_lsp_document_symbol(
|
||||||
|
symbol,
|
||||||
|
&source,
|
||||||
|
&line_index,
|
||||||
|
snapshot.encoding(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Some(lsp_types::DocumentSymbolResponse::Nested(lsp_symbols)))
|
||||||
|
} else {
|
||||||
|
// Return flattened symbols as SymbolInformation
|
||||||
|
let lsp_symbols: Vec<SymbolInformation> = symbols
|
||||||
|
.into_iter()
|
||||||
|
.map(|symbol| {
|
||||||
|
convert_to_lsp_symbol_information(
|
||||||
|
symbol,
|
||||||
|
¶ms.text_document.uri,
|
||||||
|
&source,
|
||||||
|
&line_index,
|
||||||
|
snapshot.encoding(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Some(lsp_types::DocumentSymbolResponse::Flat(lsp_symbols)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetriableRequestHandler for DocumentSymbolRequestHandler {}
|
||||||
|
|
||||||
|
fn convert_to_lsp_document_symbol(
|
||||||
|
symbol: SymbolInfo,
|
||||||
|
source: &str,
|
||||||
|
line_index: &LineIndex,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> DocumentSymbol {
|
||||||
|
let symbol_kind = convert_symbol_kind(symbol.kind);
|
||||||
|
|
||||||
|
DocumentSymbol {
|
||||||
|
name: symbol.name,
|
||||||
|
detail: None,
|
||||||
|
kind: symbol_kind,
|
||||||
|
tags: None,
|
||||||
|
#[allow(deprecated)]
|
||||||
|
deprecated: None,
|
||||||
|
range: symbol.full_range.to_lsp_range(source, line_index, encoding),
|
||||||
|
selection_range: symbol.name_range.to_lsp_range(source, line_index, encoding),
|
||||||
|
children: Some(
|
||||||
|
symbol
|
||||||
|
.children
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| convert_to_lsp_document_symbol(child, source, line_index, encoding))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
use std::panic::AssertUnwindSafe;
|
||||||
|
|
||||||
|
use lsp_types::request::WorkspaceSymbolRequest;
|
||||||
|
use lsp_types::{WorkspaceSymbolParams, WorkspaceSymbolResponse};
|
||||||
|
use ty_ide::{WorkspaceSymbolInfo, workspace_symbols};
|
||||||
|
|
||||||
|
use crate::server::api::symbols::convert_to_lsp_symbol_information;
|
||||||
|
use crate::server::api::traits::{
|
||||||
|
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||||
|
};
|
||||||
|
use crate::session::SessionSnapshot;
|
||||||
|
use crate::session::client::Client;
|
||||||
|
use crate::system::file_to_url;
|
||||||
|
use ruff_db::source::{line_index, source_text};
|
||||||
|
|
||||||
|
pub(crate) struct WorkspaceSymbolRequestHandler;
|
||||||
|
|
||||||
|
impl RequestHandler for WorkspaceSymbolRequestHandler {
|
||||||
|
type RequestType = WorkspaceSymbolRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundRequestHandler for WorkspaceSymbolRequestHandler {
|
||||||
|
fn run(
|
||||||
|
snapshot: AssertUnwindSafe<SessionSnapshot>,
|
||||||
|
_client: &Client,
|
||||||
|
params: WorkspaceSymbolParams,
|
||||||
|
) -> crate::server::Result<Option<WorkspaceSymbolResponse>> {
|
||||||
|
// Check if language services are disabled
|
||||||
|
if snapshot
|
||||||
|
.index()
|
||||||
|
.global_settings()
|
||||||
|
.is_language_services_disabled()
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = ¶ms.query;
|
||||||
|
let mut all_symbols = Vec::new();
|
||||||
|
|
||||||
|
// Iterate through all projects in the session
|
||||||
|
for db in snapshot.projects() {
|
||||||
|
// Get workspace symbols matching the query
|
||||||
|
let workspace_symbol_infos = workspace_symbols(db, query);
|
||||||
|
|
||||||
|
// Convert to LSP SymbolInformation
|
||||||
|
for workspace_symbol_info in workspace_symbol_infos {
|
||||||
|
let WorkspaceSymbolInfo { symbol, file } = workspace_symbol_info;
|
||||||
|
|
||||||
|
// Get file information for URL conversion
|
||||||
|
let source = source_text(db, file);
|
||||||
|
let line_index = line_index(db, file);
|
||||||
|
|
||||||
|
// Convert file to URL
|
||||||
|
let Some(url) = file_to_url(db, file) else {
|
||||||
|
tracing::debug!("Failed to convert file to URL at {}", file.path(db));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get position encoding from session
|
||||||
|
let encoding = snapshot.position_encoding();
|
||||||
|
|
||||||
|
let lsp_symbol =
|
||||||
|
convert_to_lsp_symbol_information(symbol, &url, &source, &line_index, encoding);
|
||||||
|
|
||||||
|
all_symbols.push(lsp_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_symbols.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(WorkspaceSymbolResponse::Flat(all_symbols)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetriableRequestHandler for WorkspaceSymbolRequestHandler {}
|
50
crates/ty_server/src/server/api/symbols.rs
Normal file
50
crates/ty_server/src/server/api/symbols.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
//! Utility functions common to language server request handlers
|
||||||
|
//! that return symbol information.
|
||||||
|
|
||||||
|
use lsp_types::{SymbolInformation, SymbolKind, Url};
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
|
use ty_ide::SymbolInfo;
|
||||||
|
|
||||||
|
use crate::document::{PositionEncoding, ToRangeExt};
|
||||||
|
|
||||||
|
/// Convert `ty_ide` `SymbolKind` to LSP `SymbolKind`
|
||||||
|
pub(crate) fn convert_symbol_kind(kind: ty_ide::SymbolKind) -> SymbolKind {
|
||||||
|
match kind {
|
||||||
|
ty_ide::SymbolKind::Module => SymbolKind::MODULE,
|
||||||
|
ty_ide::SymbolKind::Class => SymbolKind::CLASS,
|
||||||
|
ty_ide::SymbolKind::Method => SymbolKind::METHOD,
|
||||||
|
ty_ide::SymbolKind::Function => SymbolKind::FUNCTION,
|
||||||
|
ty_ide::SymbolKind::Variable => SymbolKind::VARIABLE,
|
||||||
|
ty_ide::SymbolKind::Constant => SymbolKind::CONSTANT,
|
||||||
|
ty_ide::SymbolKind::Property => SymbolKind::PROPERTY,
|
||||||
|
ty_ide::SymbolKind::Field => SymbolKind::FIELD,
|
||||||
|
ty_ide::SymbolKind::Constructor => SymbolKind::CONSTRUCTOR,
|
||||||
|
ty_ide::SymbolKind::Parameter => SymbolKind::VARIABLE,
|
||||||
|
ty_ide::SymbolKind::TypeParameter => SymbolKind::TYPE_PARAMETER,
|
||||||
|
ty_ide::SymbolKind::Import => SymbolKind::MODULE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a `ty_ide` `SymbolInfo` to LSP `SymbolInformation`
|
||||||
|
pub(crate) fn convert_to_lsp_symbol_information(
|
||||||
|
symbol: SymbolInfo,
|
||||||
|
uri: &Url,
|
||||||
|
source: &str,
|
||||||
|
line_index: &LineIndex,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> SymbolInformation {
|
||||||
|
let symbol_kind = convert_symbol_kind(symbol.kind);
|
||||||
|
|
||||||
|
SymbolInformation {
|
||||||
|
name: symbol.name,
|
||||||
|
kind: symbol_kind,
|
||||||
|
tags: None,
|
||||||
|
#[allow(deprecated)]
|
||||||
|
deprecated: None,
|
||||||
|
location: lsp_types::Location {
|
||||||
|
uri: uri.clone(),
|
||||||
|
range: symbol.full_range.to_lsp_range(source, line_index, encoding),
|
||||||
|
},
|
||||||
|
container_name: None,
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ bitflags::bitflags! {
|
||||||
const MULTILINE_SEMANTIC_TOKENS = 1 << 7;
|
const MULTILINE_SEMANTIC_TOKENS = 1 << 7;
|
||||||
const SIGNATURE_LABEL_OFFSET_SUPPORT = 1 << 8;
|
const SIGNATURE_LABEL_OFFSET_SUPPORT = 1 << 8;
|
||||||
const SIGNATURE_ACTIVE_PARAMETER_SUPPORT = 1 << 9;
|
const SIGNATURE_ACTIVE_PARAMETER_SUPPORT = 1 << 9;
|
||||||
|
const HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT = 1 << 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +71,11 @@ impl ResolvedClientCapabilities {
|
||||||
self.contains(Self::SIGNATURE_ACTIVE_PARAMETER_SUPPORT)
|
self.contains(Self::SIGNATURE_ACTIVE_PARAMETER_SUPPORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the client supports hierarchical document symbols.
|
||||||
|
pub(crate) const fn supports_hierarchical_document_symbols(self) -> bool {
|
||||||
|
self.contains(Self::HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
|
pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
|
||||||
let mut flags = Self::empty();
|
let mut flags = Self::empty();
|
||||||
|
|
||||||
|
@ -173,6 +179,18 @@ impl ResolvedClientCapabilities {
|
||||||
flags |= Self::SIGNATURE_ACTIVE_PARAMETER_SUPPORT;
|
flags |= Self::SIGNATURE_ACTIVE_PARAMETER_SUPPORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if text_document
|
||||||
|
.and_then(|text_document| {
|
||||||
|
text_document
|
||||||
|
.document_symbol
|
||||||
|
.as_ref()?
|
||||||
|
.hierarchical_document_symbol_support
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
flags |= Self::HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT;
|
||||||
|
}
|
||||||
|
|
||||||
flags
|
flags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ expression: initialization_result
|
||||||
"typeDefinitionProvider": true,
|
"typeDefinitionProvider": true,
|
||||||
"referencesProvider": true,
|
"referencesProvider": true,
|
||||||
"documentHighlightProvider": true,
|
"documentHighlightProvider": true,
|
||||||
|
"documentSymbolProvider": true,
|
||||||
|
"workspaceSymbolProvider": true,
|
||||||
"declarationProvider": true,
|
"declarationProvider": true,
|
||||||
"semanticTokensProvider": {
|
"semanticTokensProvider": {
|
||||||
"legend": {
|
"legend": {
|
||||||
|
|
|
@ -28,6 +28,8 @@ expression: initialization_result
|
||||||
"typeDefinitionProvider": true,
|
"typeDefinitionProvider": true,
|
||||||
"referencesProvider": true,
|
"referencesProvider": true,
|
||||||
"documentHighlightProvider": true,
|
"documentHighlightProvider": true,
|
||||||
|
"documentSymbolProvider": true,
|
||||||
|
"workspaceSymbolProvider": true,
|
||||||
"declarationProvider": true,
|
"declarationProvider": true,
|
||||||
"semanticTokensProvider": {
|
"semanticTokensProvider": {
|
||||||
"legend": {
|
"legend": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue