mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Add naive implementation of completions for unimported symbols
This re-works the `all_symbols` based added previously to work across all modules available, and not just what is directly in the workspace. Note that we always pass an empty string as a query, which makes the results always empty. We'll fix this in a subsequent commit.
This commit is contained in:
parent
78db56e362
commit
8e52027a88
9 changed files with 121 additions and 34 deletions
|
@ -1,6 +1,8 @@
|
||||||
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
|
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ty_project::Db;
|
use ty_project::Db;
|
||||||
|
use ty_python_semantic::all_modules;
|
||||||
|
|
||||||
|
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
|
||||||
|
|
||||||
/// Get all symbols matching the query string.
|
/// Get all symbols matching the query string.
|
||||||
///
|
///
|
||||||
|
@ -12,29 +14,29 @@ pub fn all_symbols(db: &dyn Db, query: &str) -> Vec<AllSymbolInfo> {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let project = db.project();
|
|
||||||
|
|
||||||
let query = QueryPattern::new(query);
|
let query = QueryPattern::new(query);
|
||||||
let files = project.files(db);
|
|
||||||
let results = std::sync::Mutex::new(Vec::new());
|
let results = std::sync::Mutex::new(Vec::new());
|
||||||
{
|
{
|
||||||
|
let modules = all_modules(db);
|
||||||
let db = db.dyn_clone();
|
let db = db.dyn_clone();
|
||||||
let files = &files;
|
|
||||||
let results = &results;
|
let results = &results;
|
||||||
let query = &query;
|
let query = &query;
|
||||||
|
|
||||||
rayon::scope(move |s| {
|
rayon::scope(move |s| {
|
||||||
// For each file, extract symbols and add them to results
|
// For each file, extract symbols and add them to results
|
||||||
for file in files.iter() {
|
for module in modules {
|
||||||
let db = db.dyn_clone();
|
let db = db.dyn_clone();
|
||||||
|
let Some(file) = module.file(&*db) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
s.spawn(move |_| {
|
s.spawn(move |_| {
|
||||||
for (_, symbol) in symbols_for_file_global_only(&*db, *file).search(query) {
|
for (_, symbol) in symbols_for_file_global_only(&*db, file).search(query) {
|
||||||
// It seems like we could do better here than
|
// It seems like we could do better here than
|
||||||
// locking `results` for every single symbol,
|
// locking `results` for every single symbol,
|
||||||
// but this works pretty well as it is.
|
// but this works pretty well as it is.
|
||||||
results.lock().unwrap().push(AllSymbolInfo {
|
results.lock().unwrap().push(AllSymbolInfo {
|
||||||
symbol: symbol.to_owned(),
|
symbol: symbol.to_owned(),
|
||||||
file: *file,
|
file,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -42,7 +44,13 @@ pub fn all_symbols(db: &dyn Db, query: &str) -> Vec<AllSymbolInfo> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
results.into_inner().unwrap()
|
let mut results = results.into_inner().unwrap();
|
||||||
|
results.sort_by(|s1, s2| {
|
||||||
|
let key1 = (&s1.symbol.name, s1.file.path(db).as_str());
|
||||||
|
let key2 = (&s2.symbol.name, s2.file.path(db).as_str());
|
||||||
|
key1.cmp(&key2)
|
||||||
|
});
|
||||||
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A symbol found in the workspace and dependencies, including the
|
/// A symbol found in the workspace and dependencies, including the
|
||||||
|
@ -97,17 +105,15 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_snapshot!(test.all_symbols("ufunc"), @r"
|
assert_snapshot!(test.all_symbols("acegikmo"), @r"
|
||||||
info[all-symbols]: AllSymbolInfo
|
info[all-symbols]: AllSymbolInfo
|
||||||
--> utils.py:2:5
|
--> constants.py:2:1
|
||||||
|
|
|
|
||||||
2 | ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
2 | ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
||||||
| ^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^
|
||||||
|
|
|
|
||||||
info: Function utility_function
|
info: Constant ABCDEFGHIJKLMNOP
|
||||||
");
|
|
||||||
|
|
||||||
assert_snapshot!(test.all_symbols("data"), @r"
|
|
||||||
info[all-symbols]: AllSymbolInfo
|
info[all-symbols]: AllSymbolInfo
|
||||||
--> models.py:2:7
|
--> models.py:2:7
|
||||||
|
|
|
|
||||||
|
@ -116,11 +122,10 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
||||||
3 | '''A data model class'''
|
3 | '''A data model class'''
|
||||||
4 | def __init__(self):
|
4 | def __init__(self):
|
||||||
|
|
|
|
||||||
info: Class DataModel
|
info: Class Abcdefghijklmnop
|
||||||
");
|
|
||||||
|
|
||||||
info[all-symbols]: AllSymbolInfo
|
info[all-symbols]: AllSymbolInfo
|
||||||
--> constants.py:2:1
|
--> utils.py:2:5
|
||||||
|
|
|
|
||||||
2 | def abcdefghijklmnop():
|
2 | def abcdefghijklmnop():
|
||||||
| ^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^
|
||||||
|
|
|
@ -7,10 +7,10 @@ use ruff_python_parser::{Token, TokenAt, TokenKind};
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
use ty_python_semantic::{Completion, NameKind, SemanticModel};
|
use ty_python_semantic::{Completion, NameKind, SemanticModel};
|
||||||
|
|
||||||
use crate::Db;
|
|
||||||
use crate::docstring::Docstring;
|
use crate::docstring::Docstring;
|
||||||
use crate::find_node::covering_node;
|
use crate::find_node::covering_node;
|
||||||
use crate::goto::DefinitionsOrTargets;
|
use crate::goto::DefinitionsOrTargets;
|
||||||
|
use crate::{Db, all_symbols};
|
||||||
|
|
||||||
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedCompletion<'_>> {
|
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedCompletion<'_>> {
|
||||||
let parsed = parsed_module(db, file).load(db);
|
let parsed = parsed_module(db, file).load(db);
|
||||||
|
@ -37,14 +37,27 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<DetailedComp
|
||||||
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
||||||
model.import_completions()
|
model.import_completions()
|
||||||
}
|
}
|
||||||
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
|
CompletionTargetAst::Scoped { node } => {
|
||||||
|
let mut completions = model.scoped_completions(node);
|
||||||
|
for symbol in all_symbols(db, "") {
|
||||||
|
completions.push(Completion {
|
||||||
|
name: ast::name::Name::new(&symbol.symbol.name),
|
||||||
|
ty: None,
|
||||||
|
kind: symbol.symbol.kind.to_completion_kind(),
|
||||||
|
builtin: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
completions
|
||||||
|
}
|
||||||
};
|
};
|
||||||
completions.sort_by(compare_suggestions);
|
completions.sort_by(compare_suggestions);
|
||||||
completions.dedup_by(|c1, c2| c1.name == c2.name);
|
completions.dedup_by(|c1, c2| c1.name == c2.name);
|
||||||
completions
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|completion| {
|
.map(|completion| {
|
||||||
let definition = DefinitionsOrTargets::from_ty(db, completion.ty);
|
let definition = completion
|
||||||
|
.ty
|
||||||
|
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
|
||||||
let documentation = definition.and_then(|def| def.docstring(db));
|
let documentation = definition.and_then(|def| def.docstring(db));
|
||||||
DetailedCompletion {
|
DetailedCompletion {
|
||||||
inner: completion,
|
inner: completion,
|
||||||
|
@ -3040,7 +3053,14 @@ from os.<CURSOR>
|
||||||
fn completions_without_builtins_with_types(&self) -> String {
|
fn completions_without_builtins_with_types(&self) -> String {
|
||||||
self.completions_if_snapshot(
|
self.completions_if_snapshot(
|
||||||
|c| !c.builtin,
|
|c| !c.builtin,
|
||||||
|c| format!("{} :: {}", c.name, c.ty.display(&self.db)),
|
|c| {
|
||||||
|
format!(
|
||||||
|
"{} :: {}",
|
||||||
|
c.name,
|
||||||
|
c.ty.map(|ty| ty.display(&self.db).to_string())
|
||||||
|
.unwrap_or_else(|| "Unavailable".to_string())
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor};
|
||||||
use ruff_python_ast::{Expr, Stmt};
|
use ruff_python_ast::{Expr, Stmt};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
use ty_project::Db;
|
use ty_project::Db;
|
||||||
|
use ty_python_semantic::CompletionKind;
|
||||||
|
|
||||||
/// A compiled query pattern used for searching symbols.
|
/// A compiled query pattern used for searching symbols.
|
||||||
///
|
///
|
||||||
|
@ -282,6 +283,27 @@ impl SymbolKind {
|
||||||
SymbolKind::Import => "Import",
|
SymbolKind::Import => "Import",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps this to a "completion" kind if a sensible mapping exists.
|
||||||
|
pub fn to_completion_kind(self) -> Option<CompletionKind> {
|
||||||
|
Some(match self {
|
||||||
|
SymbolKind::Module => CompletionKind::Module,
|
||||||
|
SymbolKind::Class => CompletionKind::Class,
|
||||||
|
SymbolKind::Method => CompletionKind::Method,
|
||||||
|
SymbolKind::Function => CompletionKind::Function,
|
||||||
|
SymbolKind::Variable => CompletionKind::Variable,
|
||||||
|
SymbolKind::Constant => CompletionKind::Constant,
|
||||||
|
SymbolKind::Property => CompletionKind::Property,
|
||||||
|
SymbolKind::Field => CompletionKind::Field,
|
||||||
|
SymbolKind::Constructor => CompletionKind::Constructor,
|
||||||
|
SymbolKind::Parameter => CompletionKind::Variable,
|
||||||
|
SymbolKind::TypeParameter => CompletionKind::TypeParameter,
|
||||||
|
// Not quite sure what to do with this one. I guess
|
||||||
|
// in theory the import should be "resolved" to its
|
||||||
|
// underlying kind, but that seems expensive.
|
||||||
|
SymbolKind::Import => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a flat list of symbols in the file given.
|
/// Returns a flat list of symbols in the file given.
|
||||||
|
|
|
@ -11,8 +11,8 @@ use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COM
|
||||||
pub use db::Db;
|
pub use db::Db;
|
||||||
pub use module_name::ModuleName;
|
pub use module_name::ModuleName;
|
||||||
pub use module_resolver::{
|
pub use module_resolver::{
|
||||||
Module, SearchPath, SearchPathValidationError, SearchPaths, list_modules, resolve_module,
|
Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules,
|
||||||
resolve_real_module, system_module_search_paths,
|
resolve_module, resolve_real_module, system_module_search_paths,
|
||||||
};
|
};
|
||||||
pub use program::{
|
pub use program::{
|
||||||
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
||||||
|
|
|
@ -12,6 +12,20 @@ use super::resolver::{
|
||||||
ModuleResolveMode, ResolverContext, is_non_shadowable, resolve_file_module, search_paths,
|
ModuleResolveMode, ResolverContext, is_non_shadowable, resolve_file_module, search_paths,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// List all available modules, including all sub-modules, sorted in lexicographic order.
|
||||||
|
pub fn all_modules(db: &dyn Db) -> Vec<Module<'_>> {
|
||||||
|
let mut modules = list_modules(db);
|
||||||
|
let mut stack = modules.clone();
|
||||||
|
while let Some(module) = stack.pop() {
|
||||||
|
for &submodule in module.all_submodules(db) {
|
||||||
|
modules.push(submodule);
|
||||||
|
stack.push(submodule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modules.sort_by_key(|module| module.name(db));
|
||||||
|
modules
|
||||||
|
}
|
||||||
|
|
||||||
/// List all available top-level modules.
|
/// List all available top-level modules.
|
||||||
#[salsa::tracked]
|
#[salsa::tracked]
|
||||||
pub fn list_modules(db: &dyn Db) -> Vec<Module<'_>> {
|
pub fn list_modules(db: &dyn Db) -> Vec<Module<'_>> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::iter::FusedIterator;
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
pub use list::list_modules;
|
pub use list::{all_modules, list_modules};
|
||||||
pub(crate) use module::KnownModule;
|
pub(crate) use module::KnownModule;
|
||||||
pub use module::Module;
|
pub use module::Module;
|
||||||
pub use path::{SearchPath, SearchPathValidationError};
|
pub use path::{SearchPath, SearchPathValidationError};
|
||||||
|
|
|
@ -50,7 +50,8 @@ impl<'db> SemanticModel<'db> {
|
||||||
let ty = Type::module_literal(self.db, self.file, module);
|
let ty = Type::module_literal(self.db, self.file, module);
|
||||||
Completion {
|
Completion {
|
||||||
name: Name::new(module.name(self.db).as_str()),
|
name: Name::new(module.name(self.db).as_str()),
|
||||||
ty,
|
ty: Some(ty),
|
||||||
|
kind: None,
|
||||||
builtin,
|
builtin,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -162,7 +163,12 @@ impl<'db> SemanticModel<'db> {
|
||||||
|
|
||||||
let mut completions = vec![];
|
let mut completions = vec![];
|
||||||
for crate::types::Member { name, ty } in crate::types::all_members(self.db, ty) {
|
for crate::types::Member { name, ty } in crate::types::all_members(self.db, ty) {
|
||||||
completions.push(Completion { name, ty, builtin });
|
completions.push(Completion {
|
||||||
|
name,
|
||||||
|
ty: Some(ty),
|
||||||
|
kind: None,
|
||||||
|
builtin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
completions.extend(self.submodule_completions(&module));
|
completions.extend(self.submodule_completions(&module));
|
||||||
completions
|
completions
|
||||||
|
@ -177,7 +183,8 @@ impl<'db> SemanticModel<'db> {
|
||||||
let ty = Type::module_literal(self.db, self.file, *submodule);
|
let ty = Type::module_literal(self.db, self.file, *submodule);
|
||||||
completions.push(Completion {
|
completions.push(Completion {
|
||||||
name: Name::new(submodule.name(self.db).as_str()),
|
name: Name::new(submodule.name(self.db).as_str()),
|
||||||
ty,
|
ty: Some(ty),
|
||||||
|
kind: None,
|
||||||
builtin,
|
builtin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -191,7 +198,8 @@ impl<'db> SemanticModel<'db> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|member| Completion {
|
.map(|member| Completion {
|
||||||
name: member.name,
|
name: member.name,
|
||||||
ty: member.ty,
|
ty: Some(member.ty),
|
||||||
|
kind: None,
|
||||||
builtin: false,
|
builtin: false,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -227,7 +235,8 @@ impl<'db> SemanticModel<'db> {
|
||||||
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
|
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
|
||||||
.map(|member| Completion {
|
.map(|member| Completion {
|
||||||
name: member.name,
|
name: member.name,
|
||||||
ty: member.ty,
|
ty: Some(member.ty),
|
||||||
|
kind: None,
|
||||||
builtin: false,
|
builtin: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -277,8 +286,22 @@ impl NameKind {
|
||||||
pub struct Completion<'db> {
|
pub struct Completion<'db> {
|
||||||
/// The label shown to the user for this suggestion.
|
/// The label shown to the user for this suggestion.
|
||||||
pub name: Name,
|
pub name: Name,
|
||||||
/// The type of this completion.
|
/// The type of this completion, if available.
|
||||||
pub ty: Type<'db>,
|
///
|
||||||
|
/// Generally speaking, this is always available
|
||||||
|
/// *unless* this was a completion corresponding to
|
||||||
|
/// an unimported symbol. In that case, computing the
|
||||||
|
/// type of all such symbols could be quite expensive.
|
||||||
|
pub ty: Option<Type<'db>>,
|
||||||
|
/// The "kind" of this completion.
|
||||||
|
///
|
||||||
|
/// When this is set, it takes priority over any kind
|
||||||
|
/// inferred from `ty`.
|
||||||
|
///
|
||||||
|
/// Usually this is set when `ty` is `None`, since it
|
||||||
|
/// may be cheaper to compute at scale. (e.g., For
|
||||||
|
/// unimported symbol completions.)
|
||||||
|
pub kind: Option<CompletionKind>,
|
||||||
/// Whether this suggestion came from builtins or not.
|
/// Whether this suggestion came from builtins or not.
|
||||||
///
|
///
|
||||||
/// At time of writing (2025-06-26), this information
|
/// At time of writing (2025-06-26), this information
|
||||||
|
@ -336,7 +359,7 @@ impl<'db> Completion<'db> {
|
||||||
Type::TypeAlias(alias) => imp(db, alias.value_type(db))?,
|
Type::TypeAlias(alias) => imp(db, alias.value_type(db))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
imp(db, self.ty)
|
self.kind.or_else(|| self.ty.and_then(|ty| imp(db, ty)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
||||||
label: comp.inner.name.into(),
|
label: comp.inner.name.into(),
|
||||||
kind,
|
kind,
|
||||||
sort_text: Some(format!("{i:-max_index_len$}")),
|
sort_text: Some(format!("{i:-max_index_len$}")),
|
||||||
detail: comp.inner.ty.display(db).to_string().into(),
|
detail: comp.inner.ty.map(|ty| ty.display(db).to_string()),
|
||||||
documentation: comp
|
documentation: comp
|
||||||
.documentation
|
.documentation
|
||||||
.map(|docstring| Documentation::String(docstring.render_plaintext())),
|
.map(|docstring| Documentation::String(docstring.render_plaintext())),
|
||||||
|
|
|
@ -425,7 +425,10 @@ impl Workspace {
|
||||||
documentation: completion
|
documentation: completion
|
||||||
.documentation
|
.documentation
|
||||||
.map(|documentation| documentation.render_plaintext()),
|
.map(|documentation| documentation.render_plaintext()),
|
||||||
detail: completion.inner.ty.display(&self.db).to_string().into(),
|
detail: completion
|
||||||
|
.inner
|
||||||
|
.ty
|
||||||
|
.map(|ty| ty.display(&self.db).to_string()),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue