mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 22:31:23 +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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue