diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index 6b78a8c367..02ed56d6db 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -1,6 +1,8 @@ -use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only}; use ruff_db::files::File; 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. /// @@ -12,29 +14,29 @@ pub fn all_symbols(db: &dyn Db, query: &str) -> Vec { return Vec::new(); } - let project = db.project(); - let query = QueryPattern::new(query); - let files = project.files(db); let results = std::sync::Mutex::new(Vec::new()); { + let modules = all_modules(db); let db = db.dyn_clone(); - let files = &files; let results = &results; let query = &query; rayon::scope(move |s| { // 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 Some(file) = module.file(&*db) else { + continue; + }; 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 // locking `results` for every single symbol, // but this works pretty well as it is. results.lock().unwrap().push(AllSymbolInfo { symbol: symbol.to_owned(), - file: *file, + file, }); } }); @@ -42,7 +44,13 @@ pub fn all_symbols(db: &dyn Db, query: &str) -> Vec { }); } - 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 @@ -97,17 +105,15 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' ) .build(); - assert_snapshot!(test.all_symbols("ufunc"), @r" + assert_snapshot!(test.all_symbols("acegikmo"), @r" info[all-symbols]: AllSymbolInfo - --> utils.py:2:5 + --> constants.py:2:1 | 2 | ABCDEFGHIJKLMNOP = 'https://api.example.com' | ^^^^^^^^^^^^^^^^ | - info: Function utility_function - "); + info: Constant ABCDEFGHIJKLMNOP - assert_snapshot!(test.all_symbols("data"), @r" info[all-symbols]: AllSymbolInfo --> models.py:2:7 | @@ -116,11 +122,10 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' 3 | '''A data model class''' 4 | def __init__(self): | - info: Class DataModel - "); + info: Class Abcdefghijklmnop info[all-symbols]: AllSymbolInfo - --> constants.py:2:1 + --> utils.py:2:5 | 2 | def abcdefghijklmnop(): | ^^^^^^^^^^^^^^^^ diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 11f648ebd7..4d9959e9e2 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -7,10 +7,10 @@ use ruff_python_parser::{Token, TokenAt, TokenKind}; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::{Completion, NameKind, SemanticModel}; -use crate::Db; use crate::docstring::Docstring; use crate::find_node::covering_node; use crate::goto::DefinitionsOrTargets; +use crate::{Db, all_symbols}; pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec> { let parsed = parsed_module(db, file).load(db); @@ -37,14 +37,27 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec { 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.dedup_by(|c1, c2| c1.name == c2.name); completions .into_iter() .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)); DetailedCompletion { inner: completion, @@ -3040,7 +3053,14 @@ from os. fn completions_without_builtins_with_types(&self) -> String { self.completions_if_snapshot( |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()) + ) + }, ) } diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 646c89537b..6ba3d7101d 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -13,6 +13,7 @@ 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; +use ty_python_semantic::CompletionKind; /// A compiled query pattern used for searching symbols. /// @@ -282,6 +283,27 @@ impl SymbolKind { SymbolKind::Import => "Import", } } + + /// Maps this to a "completion" kind if a sensible mapping exists. + pub fn to_completion_kind(self) -> Option { + 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. diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index cf58ab93a6..48a2cc9eb1 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -11,8 +11,8 @@ use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COM pub use db::Db; pub use module_name::ModuleName; pub use module_resolver::{ - Module, SearchPath, SearchPathValidationError, SearchPaths, list_modules, resolve_module, - resolve_real_module, system_module_search_paths, + Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules, + resolve_module, resolve_real_module, system_module_search_paths, }; pub use program::{ Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource, diff --git a/crates/ty_python_semantic/src/module_resolver/list.rs b/crates/ty_python_semantic/src/module_resolver/list.rs index 0d0f95140c..bcbad13534 100644 --- a/crates/ty_python_semantic/src/module_resolver/list.rs +++ b/crates/ty_python_semantic/src/module_resolver/list.rs @@ -12,6 +12,20 @@ use super::resolver::{ 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> { + 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. #[salsa::tracked] pub fn list_modules(db: &dyn Db) -> Vec> { diff --git a/crates/ty_python_semantic/src/module_resolver/mod.rs b/crates/ty_python_semantic/src/module_resolver/mod.rs index 549ffa2c96..9beec6b385 100644 --- a/crates/ty_python_semantic/src/module_resolver/mod.rs +++ b/crates/ty_python_semantic/src/module_resolver/mod.rs @@ -1,6 +1,6 @@ use std::iter::FusedIterator; -pub use list::list_modules; +pub use list::{all_modules, list_modules}; pub(crate) use module::KnownModule; pub use module::Module; pub use path::{SearchPath, SearchPathValidationError}; diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index ba633b5d22..fc6bf60c4b 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -50,7 +50,8 @@ impl<'db> SemanticModel<'db> { let ty = Type::module_literal(self.db, self.file, module); Completion { name: Name::new(module.name(self.db).as_str()), - ty, + ty: Some(ty), + kind: None, builtin, } }) @@ -162,7 +163,12 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; 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 @@ -177,7 +183,8 @@ impl<'db> SemanticModel<'db> { let ty = Type::module_literal(self.db, self.file, *submodule); completions.push(Completion { name: Name::new(submodule.name(self.db).as_str()), - ty, + ty: Some(ty), + kind: None, builtin, }); } @@ -191,7 +198,8 @@ impl<'db> SemanticModel<'db> { .into_iter() .map(|member| Completion { name: member.name, - ty: member.ty, + ty: Some(member.ty), + kind: None, builtin: false, }) .collect() @@ -227,7 +235,8 @@ impl<'db> SemanticModel<'db> { all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) .map(|member| Completion { name: member.name, - ty: member.ty, + ty: Some(member.ty), + kind: None, builtin: false, }), ); @@ -277,8 +286,22 @@ impl NameKind { pub struct Completion<'db> { /// The label shown to the user for this suggestion. pub name: Name, - /// The type of this completion. - pub ty: Type<'db>, + /// The type of this completion, if available. + /// + /// 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>, + /// 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, /// Whether this suggestion came from builtins or not. /// /// 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))?, }) } - imp(db, self.ty) + self.kind.or_else(|| self.ty.and_then(|ty| imp(db, ty))) } } diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index e175ed49fe..c2c5d49e4e 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -71,7 +71,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { label: comp.inner.name.into(), kind, 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 .map(|docstring| Documentation::String(docstring.render_plaintext())), diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 8722afcbe7..cf88ce6392 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -425,7 +425,10 @@ impl Workspace { documentation: completion .documentation .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()) }