Skip to content

Commit cdef3f5

Browse files
committed
[ty] Use dedicated collector for completions
This is a small refactor that helps centralize the logic for how we gather, convert and possibly filter completions. Some of this logic was spread out before, which motivated this refactor. Moreover, as part of other refactoring, I found myself chaffing against the lack of this abstraction.
1 parent 6178822 commit cdef3f5

File tree

3 files changed

+119
-37
lines changed

3 files changed

+119
-37
lines changed

crates/ty_ide/src/all_symbols.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
88
///
99
/// Returns symbols from all files in the workspace and dependencies, filtered
1010
/// by the query.
11-
pub fn all_symbols<'db>(db: &'db dyn Db, query: &str) -> Vec<AllSymbolInfo<'db>> {
11+
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
1212
// If the query is empty, return immediately to avoid expensive file scanning
13-
if query.is_empty() {
13+
if query.will_match_everything() {
1414
return Vec::new();
1515
}
1616

17-
let query = QueryPattern::new(query);
1817
let results = std::sync::Mutex::new(Vec::new());
1918
{
2019
let modules = all_modules(db);
@@ -144,7 +143,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
144143

145144
impl CursorTest {
146145
fn all_symbols(&self, query: &str) -> String {
147-
let symbols = all_symbols(&self.db, query);
146+
let symbols = all_symbols(&self.db, &QueryPattern::new(query));
148147

149148
if symbols.is_empty() {
150149
return "No symbols found".to_string();

crates/ty_ide/src/completion.rs

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,91 @@ use crate::importer::{ImportRequest, Importer};
2121
use crate::symbols::QueryPattern;
2222
use crate::{Db, all_symbols};
2323

24+
/// A collection of completions built up from various sources.
25+
#[derive(Clone)]
26+
struct Completions<'db> {
27+
db: &'db dyn Db,
28+
items: Vec<Completion<'db>>,
29+
query: QueryPattern,
30+
}
31+
32+
impl<'db> Completions<'db> {
33+
/// Create a new empty collection of completions.
34+
///
35+
/// The given typed text should correspond to what we believe
36+
/// the user has typed as part of the next symbol they are writing.
37+
/// This collection will treat it as a query when present, and only
38+
/// add completions that match it.
39+
fn new(db: &'db dyn Db, typed: Option<&str>) -> Completions<'db> {
40+
let query = typed
41+
.map(QueryPattern::new)
42+
.unwrap_or_else(QueryPattern::matches_all_symbols);
43+
Completions {
44+
db,
45+
items: vec![],
46+
query,
47+
}
48+
}
49+
50+
/// Convert this collection into a simple
51+
/// sequence of completions.
52+
fn into_completions(mut self) -> Vec<Completion<'db>> {
53+
self.items.sort_by(compare_suggestions);
54+
self.items
55+
.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
56+
self.items
57+
}
58+
59+
/// Attempts to adds the given completion to this collection.
60+
///
61+
/// When added, `true` is returned.
62+
///
63+
/// This might not add the completion for a variety of reasons.
64+
/// For example, if the symbol name does not match this collection's
65+
/// query.
66+
fn try_add(&mut self, completion: Completion<'db>) -> bool {
67+
if !self.query.is_match_symbol_name(completion.name.as_str()) {
68+
return false;
69+
}
70+
self.force_add(completion);
71+
true
72+
}
73+
74+
/// Attempts to adds the given semantic completion to this collection.
75+
///
76+
/// When added, `true` is returned.
77+
fn try_add_semantic(&mut self, completion: SemanticCompletion<'db>) -> bool {
78+
self.try_add(Completion::from_semantic_completion(self.db, completion))
79+
}
80+
81+
/// Always adds the given completion to this collection.
82+
fn force_add(&mut self, completion: Completion<'db>) {
83+
self.items.push(completion);
84+
}
85+
}
86+
87+
impl<'db> Extend<SemanticCompletion<'db>> for Completions<'db> {
88+
fn extend<T>(&mut self, it: T)
89+
where
90+
T: IntoIterator<Item = SemanticCompletion<'db>>,
91+
{
92+
for c in it {
93+
self.try_add_semantic(c);
94+
}
95+
}
96+
}
97+
98+
impl<'db> Extend<Completion<'db>> for Completions<'db> {
99+
fn extend<T>(&mut self, it: T)
100+
where
101+
T: IntoIterator<Item = Completion<'db>>,
102+
{
103+
for c in it {
104+
self.try_add(c);
105+
}
106+
}
107+
}
108+
24109
#[derive(Clone, Debug)]
25110
pub struct Completion<'db> {
26111
/// The label shown to the user for this suggestion.
@@ -251,11 +336,6 @@ pub fn completion<'db>(
251336
return vec![completions];
252337
}
253338

254-
let typed_query = typed
255-
.as_deref()
256-
.map(QueryPattern::new)
257-
.unwrap_or_else(QueryPattern::matches_all_symbols);
258-
259339
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
260340
return vec![];
261341
};
@@ -282,14 +362,12 @@ pub fn completion<'db>(
282362
(model.scoped_completions(scoped.node), Some(scoped))
283363
}
284364
};
285-
let mut completions: Vec<Completion<'_>> = semantic_completions
286-
.into_iter()
287-
.filter(|c| typed_query.is_match_symbol_name(c.name.as_str()))
288-
.map(|c| Completion::from_semantic_completion(db, c))
289-
.collect();
365+
366+
let mut completions = Completions::new(db, typed.as_deref());
367+
completions.extend(semantic_completions);
290368

291369
if scoped.is_some() {
292-
add_keyword_completions(db, &typed_query, &mut completions);
370+
add_keyword_completions(db, &mut completions);
293371
}
294372
if settings.auto_import {
295373
if let Some(scoped) = scoped {
@@ -303,31 +381,22 @@ pub fn completion<'db>(
303381
);
304382
}
305383
}
306-
completions.sort_by(compare_suggestions);
307-
completions.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
308-
completions
384+
completions.into_completions()
309385
}
310386

311387
/// Adds completions derived from keywords.
312388
///
313389
/// This should generally only be used when offering "scoped" completions.
314390
/// This will include keywords corresponding to Python values (like `None`)
315391
/// and general language keywords (like `raise`).
316-
fn add_keyword_completions<'db>(
317-
db: &'db dyn Db,
318-
query: &QueryPattern,
319-
completions: &mut Vec<Completion<'db>>,
320-
) {
392+
fn add_keyword_completions<'db>(db: &'db dyn Db, completions: &mut Completions<'db>) {
321393
let keyword_values = [
322394
("None", Type::none(db)),
323395
("True", Type::BooleanLiteral(true)),
324396
("False", Type::BooleanLiteral(false)),
325397
];
326398
for (name, ty) in keyword_values {
327-
if !query.is_match_symbol_name(name) {
328-
continue;
329-
}
330-
completions.push(Completion::value_keyword(name, ty));
399+
completions.try_add(Completion::value_keyword(name, ty));
331400
}
332401

333402
// Note that we specifically omit the `type` keyword here, since
@@ -343,10 +412,7 @@ fn add_keyword_completions<'db>(
343412
"yield", "case", "match",
344413
];
345414
for name in keywords {
346-
if !query.is_match_symbol_name(name) {
347-
continue;
348-
}
349-
completions.push(Completion::keyword(name));
415+
completions.try_add(Completion::keyword(name));
350416
}
351417
}
352418

@@ -377,17 +443,22 @@ fn add_unimported_completions<'db>(
377443
parsed: &ParsedModuleRef,
378444
scoped: ScopedTarget<'_>,
379445
typed: Option<&str>,
380-
completions: &mut Vec<Completion<'db>>,
446+
completions: &mut Completions<'db>,
381447
) {
382-
let Some(typed) = typed else {
448+
// This is redundant since `all_symbols` will also bail
449+
// when the query can match everything. But we bail here
450+
// to avoid building an `Importer` and other plausibly
451+
// costly work when we know we won't use it.
452+
if completions.query.will_match_everything() {
383453
return;
384-
};
454+
}
455+
385456
let source = source_text(db, file);
386457
let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str());
387458
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
388459
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
389460

390-
for symbol in all_symbols(db, typed) {
461+
for symbol in all_symbols(db, &completions.query) {
391462
if symbol.module.file(db) == Some(file) {
392463
continue;
393464
}
@@ -399,7 +470,7 @@ fn add_unimported_completions<'db>(
399470
// "fine," but it might mean that we import a symbol from the
400471
// "wrong" module.
401472
let import_action = importer.import(request, &members);
402-
completions.push(Completion {
473+
completions.add(Completion {
403474
name: ast::name::Name::new(&symbol.symbol.name),
404475
insert: Some(import_action.symbol_text().into()),
405476
ty: None,
@@ -975,6 +1046,8 @@ fn is_import_alias_incomplete(tokens: &[Token], typed: Option<&str>) -> bool {
9751046
///
9761047
/// If there isn't any typed text or it could not otherwise be found,
9771048
/// then `None` is returned.
1049+
///
1050+
/// When `Some` is returned, the string is guaranteed to be non-empty.
9781051
fn find_typed_text(
9791052
db: &dyn Db,
9801053
file: File,
@@ -997,7 +1070,7 @@ fn find_typed_text(
9971070
// been typed. This likely means there is whitespace
9981071
// or something that isn't represented in the token
9991072
// stream. So just give up.
1000-
if last.end() < offset {
1073+
if last.end() < offset || last.range().is_empty() {
10011074
return None;
10021075
}
10031076
Some(source[last.range()].to_string())

crates/ty_ide/src/symbols.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ impl QueryPattern {
6767
symbol_name.contains(&self.original)
6868
}
6969
}
70+
71+
/// Returns true when it is known that this pattern will return `true` for
72+
/// all inputs given to `QueryPattern::is_match_symbol_name`.
73+
///
74+
/// This will never return `true` incorrectly, but it may return `false`
75+
/// incorrectly. That is, it's possible that this query will match all
76+
/// inputs but this still returns `false`.
77+
pub fn will_match_everything(&self) -> bool {
78+
self.re.is_none()
79+
}
7080
}
7181

7282
impl From<&str> for QueryPattern {

0 commit comments

Comments
 (0)