mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:14:52 +00:00
[ty] Refactor to handle unimported completions
This rejiggers some stuff in the main completions entrypoint in `ty_ide`. A more refined `Completion` type is defined with more information. In particular, to support auto-import, we now include a module name and an "edit" for inserting an import. This also rolls the old "detailed completion" into the new completion type. Previously, we were relying on the completion type for `ty_python_semantic`. But `ty_ide` is really the code that owns completions. Note that this code doesn't build as-is. The next commit will add the importer used here in `add_unimported_completions`.
This commit is contained in:
parent
02ee22db78
commit
bcc8d6910b
4 changed files with 182 additions and 71 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4237,6 +4237,8 @@ dependencies = [
|
||||||
"ruff_index",
|
"ruff_index",
|
||||||
"ruff_memory_usage",
|
"ruff_memory_usage",
|
||||||
"ruff_python_ast",
|
"ruff_python_ast",
|
||||||
|
"ruff_python_codegen",
|
||||||
|
"ruff_python_importer",
|
||||||
"ruff_python_parser",
|
"ruff_python_parser",
|
||||||
"ruff_python_trivia",
|
"ruff_python_trivia",
|
||||||
"ruff_source_file",
|
"ruff_source_file",
|
||||||
|
|
|
@ -16,6 +16,8 @@ ruff_db = { workspace = true }
|
||||||
ruff_index = { workspace = true }
|
ruff_index = { workspace = true }
|
||||||
ruff_memory_usage = { workspace = true }
|
ruff_memory_usage = { workspace = true }
|
||||||
ruff_python_ast = { workspace = true }
|
ruff_python_ast = { workspace = true }
|
||||||
|
ruff_python_codegen = { workspace = true }
|
||||||
|
ruff_python_importer = { workspace = true }
|
||||||
ruff_python_parser = { workspace = true }
|
ruff_python_parser = { workspace = true }
|
||||||
ruff_python_trivia = { workspace = true }
|
ruff_python_trivia = { workspace = true }
|
||||||
ruff_source_file = { workspace = true }
|
ruff_source_file = { workspace = true }
|
||||||
|
|
|
@ -2,7 +2,11 @@ use std::cmp::Ordering;
|
||||||
|
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||||
|
use ruff_db::source::source_text;
|
||||||
|
use ruff_diagnostics::Edit;
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
|
use ruff_python_ast::name::Name;
|
||||||
|
use ruff_python_codegen::Stylist;
|
||||||
use ruff_python_parser::{Token, TokenAt, TokenKind};
|
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::{
|
use ty_python_semantic::{
|
||||||
|
@ -13,9 +17,79 @@ use ty_python_semantic::{
|
||||||
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::importer::{ImportRequest, Importer};
|
||||||
use crate::{Db, all_symbols};
|
use crate::{Db, all_symbols};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Completion<'db> {
|
||||||
|
/// The label shown to the user for this suggestion.
|
||||||
|
pub name: Name,
|
||||||
|
/// The text that should be inserted at the cursor
|
||||||
|
/// when the completion is selected.
|
||||||
|
///
|
||||||
|
/// When this is not set, `name` is used.
|
||||||
|
pub insert: Option<Box<str>>,
|
||||||
|
/// 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<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).
|
||||||
|
///
|
||||||
|
/// Callers should use [`Completion::kind`] to get the
|
||||||
|
/// kind, which will take type information into account
|
||||||
|
/// if this kind is not present.
|
||||||
|
pub kind: Option<CompletionKind>,
|
||||||
|
/// The name of the module that this completion comes from.
|
||||||
|
///
|
||||||
|
/// This is generally only present when this is a completion
|
||||||
|
/// suggestion for an unimported symbol.
|
||||||
|
pub module_name: Option<&'db ModuleName>,
|
||||||
|
/// An import statement to insert (or ensure is already
|
||||||
|
/// present) when this completion is selected.
|
||||||
|
pub import: Option<Edit>,
|
||||||
|
/// Whether this suggestion came from builtins or not.
|
||||||
|
///
|
||||||
|
/// At time of writing (2025-06-26), this information
|
||||||
|
/// doesn't make it into the LSP response. Instead, we
|
||||||
|
/// use it mainly in tests so that we can write less
|
||||||
|
/// noisy tests.
|
||||||
|
pub builtin: bool,
|
||||||
|
/// The documentation associated with this item, if
|
||||||
|
/// available.
|
||||||
|
pub documentation: Option<Docstring>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'db> Completion<'db> {
|
impl<'db> Completion<'db> {
|
||||||
|
fn from_semantic_completion(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
semantic: SemanticCompletion<'db>,
|
||||||
|
) -> Completion<'db> {
|
||||||
|
let definition = semantic
|
||||||
|
.ty
|
||||||
|
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
|
||||||
|
let documentation = definition.and_then(|def| def.docstring(db));
|
||||||
|
Completion {
|
||||||
|
name: semantic.name,
|
||||||
|
insert: None,
|
||||||
|
ty: semantic.ty,
|
||||||
|
kind: None,
|
||||||
|
module_name: None,
|
||||||
|
import: None,
|
||||||
|
builtin: semantic.builtin,
|
||||||
|
documentation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the "kind" of this completion.
|
/// Returns the "kind" of this completion.
|
||||||
///
|
///
|
||||||
/// This is meant to be a very general classification of this completion.
|
/// This is meant to be a very general classification of this completion.
|
||||||
|
@ -130,7 +204,7 @@ pub fn completion<'db>(
|
||||||
settings: &CompletionSettings,
|
settings: &CompletionSettings,
|
||||||
file: File,
|
file: File,
|
||||||
offset: TextSize,
|
offset: TextSize,
|
||||||
) -> Vec<DetailedCompletion<'db>> {
|
) -> Vec<Completion<'db>> {
|
||||||
let parsed = parsed_module(db, file).load(db);
|
let parsed = parsed_module(db, file).load(db);
|
||||||
|
|
||||||
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
|
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
|
||||||
|
@ -141,64 +215,79 @@ pub fn completion<'db>(
|
||||||
};
|
};
|
||||||
|
|
||||||
let model = SemanticModel::new(db, file);
|
let model = SemanticModel::new(db, file);
|
||||||
let mut completions = match target {
|
let (semantic_completions, scoped) = match target {
|
||||||
CompletionTargetAst::ObjectDot { expr } => model.attribute_completions(expr),
|
CompletionTargetAst::ObjectDot { expr } => (model.attribute_completions(expr), None),
|
||||||
CompletionTargetAst::ObjectDotInImport { import, name } => {
|
CompletionTargetAst::ObjectDotInImport { import, name } => {
|
||||||
model.import_submodule_completions(import, name)
|
(model.import_submodule_completions(import, name), None)
|
||||||
}
|
}
|
||||||
CompletionTargetAst::ObjectDotInImportFrom { import } => {
|
CompletionTargetAst::ObjectDotInImportFrom { import } => {
|
||||||
model.from_import_submodule_completions(import)
|
(model.from_import_submodule_completions(import), None)
|
||||||
}
|
}
|
||||||
CompletionTargetAst::ImportFrom { import, name } => {
|
CompletionTargetAst::ImportFrom { import, name } => {
|
||||||
model.from_import_completions(import, name)
|
(model.from_import_completions(import, name), None)
|
||||||
}
|
}
|
||||||
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
||||||
model.import_completions()
|
(model.import_completions(), None)
|
||||||
}
|
}
|
||||||
CompletionTargetAst::Scoped { node, typed } => {
|
CompletionTargetAst::Scoped(scoped) => {
|
||||||
let mut completions = model.scoped_completions(node);
|
(model.scoped_completions(scoped.node), Some(scoped))
|
||||||
if settings.auto_import {
|
|
||||||
if let Some(typed) = typed {
|
|
||||||
for symbol in all_symbols(db, typed) {
|
|
||||||
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);
|
let mut completions: Vec<Completion<'_>> = semantic_completions
|
||||||
completions.dedup_by(|c1, c2| c1.name == c2.name);
|
|
||||||
completions
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|completion| {
|
.map(|c| Completion::from_semantic_completion(db, c))
|
||||||
let definition = completion
|
.collect();
|
||||||
.ty
|
|
||||||
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
|
if settings.auto_import {
|
||||||
let documentation = definition.and_then(|def| def.docstring(db));
|
if let Some(scoped) = scoped {
|
||||||
DetailedCompletion {
|
add_unimported_completions(db, file, &parsed, scoped, &mut completions);
|
||||||
inner: completion,
|
|
||||||
documentation,
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.collect()
|
completions.sort_by(compare_suggestions);
|
||||||
|
completions.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
|
||||||
|
completions
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
/// Adds completions not in scope.
|
||||||
pub struct DetailedCompletion<'db> {
|
///
|
||||||
pub inner: Completion<'db>,
|
/// `scoped` should be information about the identified scope
|
||||||
pub documentation: Option<Docstring>,
|
/// in which the cursor is currently placed.
|
||||||
}
|
///
|
||||||
|
/// The completions returned will auto-insert import statements
|
||||||
|
/// when selected into `File`.
|
||||||
|
fn add_unimported_completions<'db>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
file: File,
|
||||||
|
parsed: &ParsedModuleRef,
|
||||||
|
scoped: ScopedTarget<'_>,
|
||||||
|
completions: &mut Vec<Completion<'db>>,
|
||||||
|
) {
|
||||||
|
let Some(typed) = scoped.typed else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let source = source_text(db, file);
|
||||||
|
let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str());
|
||||||
|
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
|
||||||
|
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
|
||||||
|
|
||||||
impl<'db> std::ops::Deref for DetailedCompletion<'db> {
|
for symbol in all_symbols(db, typed) {
|
||||||
type Target = Completion<'db>;
|
let request =
|
||||||
fn deref(&self) -> &Self::Target {
|
ImportRequest::import_from(symbol.module.name(db).as_str(), &symbol.symbol.name);
|
||||||
&self.inner
|
// FIXME: `all_symbols` doesn't account for wildcard imports.
|
||||||
|
// Since we're looking at every module, this is probably
|
||||||
|
// "fine," but it might mean that we import a symbol from the
|
||||||
|
// "wrong" module.
|
||||||
|
let import_action = importer.import(request, &members);
|
||||||
|
completions.push(Completion {
|
||||||
|
name: ast::name::Name::new(&symbol.symbol.name),
|
||||||
|
insert: Some(import_action.symbol_text().into()),
|
||||||
|
ty: None,
|
||||||
|
kind: symbol.symbol.kind.to_completion_kind(),
|
||||||
|
module_name: Some(symbol.module.name(db)),
|
||||||
|
import: import_action.import().cloned(),
|
||||||
|
builtin: false,
|
||||||
|
documentation: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,15 +492,15 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
Some(CompletionTargetAst::Scoped { node, typed })
|
Some(CompletionTargetAst::Scoped(ScopedTarget { node, typed }))
|
||||||
}
|
}
|
||||||
CompletionTargetTokens::Unknown => {
|
CompletionTargetTokens::Unknown => {
|
||||||
let range = TextRange::empty(offset);
|
let range = TextRange::empty(offset);
|
||||||
let covering_node = covering_node(parsed.syntax().into(), range);
|
let covering_node = covering_node(parsed.syntax().into(), range);
|
||||||
Some(CompletionTargetAst::Scoped {
|
Some(CompletionTargetAst::Scoped(ScopedTarget {
|
||||||
node: covering_node.node(),
|
node: covering_node.node(),
|
||||||
typed: None,
|
typed: None,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -464,7 +553,11 @@ enum CompletionTargetAst<'t> {
|
||||||
},
|
},
|
||||||
/// A scoped scenario, where we want to list all items available in
|
/// A scoped scenario, where we want to list all items available in
|
||||||
/// the most narrow scope containing the giving AST node.
|
/// the most narrow scope containing the giving AST node.
|
||||||
Scoped {
|
Scoped(ScopedTarget<'t>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct ScopedTarget<'t> {
|
||||||
/// The node with the smallest range that fully covers
|
/// The node with the smallest range that fully covers
|
||||||
/// the token under the cursor.
|
/// the token under the cursor.
|
||||||
node: ast::AnyNodeRef<'t>,
|
node: ast::AnyNodeRef<'t>,
|
||||||
|
@ -473,7 +566,6 @@ enum CompletionTargetAst<'t> {
|
||||||
/// When not `None`, the typed text is guaranteed to be
|
/// When not `None`, the typed text is guaranteed to be
|
||||||
/// non-empty.
|
/// non-empty.
|
||||||
typed: Option<&'t str>,
|
typed: Option<&'t str>,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a suffix of `tokens` corresponding to the `kinds` given.
|
/// Returns a suffix of `tokens` corresponding to the `kinds` given.
|
||||||
|
@ -654,12 +746,10 @@ mod tests {
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
use ruff_python_parser::{Mode, ParseOptions, TokenKind, Tokens};
|
use ruff_python_parser::{Mode, ParseOptions, TokenKind, Tokens};
|
||||||
|
|
||||||
use crate::completion::{DetailedCompletion, completion};
|
use crate::completion::{Completion, completion};
|
||||||
use crate::tests::{CursorTest, cursor_test};
|
use crate::tests::{CursorTest, cursor_test};
|
||||||
|
|
||||||
use super::{CompletionSettings, token_suffix_by_kinds};
|
use super::{CompletionKind, CompletionSettings, token_suffix_by_kinds};
|
||||||
|
|
||||||
use ty_python_semantic::CompletionKind;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn token_suffixes_match() {
|
fn token_suffixes_match() {
|
||||||
|
@ -3248,14 +3338,14 @@ from os.<CURSOR>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completions_if(&self, predicate: impl Fn(&DetailedCompletion) -> bool) -> String {
|
fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String {
|
||||||
self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string())
|
self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completions_if_snapshot(
|
fn completions_if_snapshot(
|
||||||
&self,
|
&self,
|
||||||
predicate: impl Fn(&DetailedCompletion) -> bool,
|
predicate: impl Fn(&Completion) -> bool,
|
||||||
snapshot: impl Fn(&DetailedCompletion) -> String,
|
snapshot: impl Fn(&Completion) -> String,
|
||||||
) -> String {
|
) -> String {
|
||||||
let settings = CompletionSettings::default();
|
let settings = CompletionSettings::default();
|
||||||
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
|
let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset);
|
||||||
|
|
|
@ -3,15 +3,16 @@ use std::time::Instant;
|
||||||
|
|
||||||
use lsp_types::request::Completion;
|
use lsp_types::request::Completion;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, Documentation, Url,
|
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
|
||||||
|
CompletionResponse, Documentation, TextEdit, Url,
|
||||||
};
|
};
|
||||||
use ruff_db::source::{line_index, source_text};
|
use ruff_db::source::{line_index, source_text};
|
||||||
use ruff_source_file::OneIndexed;
|
use ruff_source_file::OneIndexed;
|
||||||
use ty_ide::{CompletionSettings, completion};
|
use ruff_text_size::Ranged;
|
||||||
|
use ty_ide::{CompletionKind, CompletionSettings, completion};
|
||||||
use ty_project::ProjectDatabase;
|
use ty_project::ProjectDatabase;
|
||||||
use ty_python_semantic::CompletionKind;
|
|
||||||
|
|
||||||
use crate::document::PositionExt;
|
use crate::document::{PositionExt, ToRangeExt};
|
||||||
use crate::server::api::traits::{
|
use crate::server::api::traits::{
|
||||||
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||||
};
|
};
|
||||||
|
@ -70,11 +71,27 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, comp)| {
|
.map(|(i, comp)| {
|
||||||
let kind = comp.kind(db).map(ty_kind_to_lsp_kind);
|
let kind = comp.kind(db).map(ty_kind_to_lsp_kind);
|
||||||
|
let type_display = comp.ty.map(|ty| ty.display(db).to_string());
|
||||||
|
let import_edit = comp.import.as_ref().map(|edit| {
|
||||||
|
let range =
|
||||||
|
edit.range()
|
||||||
|
.to_lsp_range(&source, &line_index, snapshot.encoding());
|
||||||
|
TextEdit {
|
||||||
|
range,
|
||||||
|
new_text: edit.content().map(ToString::to_string).unwrap_or_default(),
|
||||||
|
}
|
||||||
|
});
|
||||||
CompletionItem {
|
CompletionItem {
|
||||||
label: comp.inner.name.into(),
|
label: comp.name.into(),
|
||||||
kind,
|
kind,
|
||||||
sort_text: Some(format!("{i:-max_index_len$}")),
|
sort_text: Some(format!("{i:-max_index_len$}")),
|
||||||
detail: comp.inner.ty.map(|ty| ty.display(db).to_string()),
|
detail: type_display.clone(),
|
||||||
|
label_details: Some(CompletionItemLabelDetails {
|
||||||
|
detail: type_display,
|
||||||
|
description: comp.module_name.map(ToString::to_string),
|
||||||
|
}),
|
||||||
|
insert_text: comp.insert.map(String::from),
|
||||||
|
additional_text_edits: import_edit.map(|edit| vec![edit]),
|
||||||
documentation: comp
|
documentation: comp
|
||||||
.documentation
|
.documentation
|
||||||
.map(|docstring| Documentation::String(docstring.render_plaintext())),
|
.map(|docstring| Documentation::String(docstring.render_plaintext())),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue