[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:
Andrew Gallant 2025-08-28 14:52:50 -04:00 committed by Andrew Gallant
parent 78db56e362
commit 8e52027a88
9 changed files with 121 additions and 34 deletions

View file

@ -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():
| ^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^

View file

@ -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())
)
},
) )
} }

View file

@ -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.

View file

@ -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,

View file

@ -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<'_>> {

View file

@ -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};

View file

@ -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)))
} }
} }

View file

@ -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())),

View file

@ -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())
} }