use crate::{
render::{render_resolution_with_import, RenderContext},
- CompletionContext, Completions,
+ CompletionContext, Completions, ImportEdit,
};
pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionContext) {
acc.add_resolution(ctx, name.to_string(), &res)
});
- if ctx.config.enable_experimental_completions {
+ if ctx.config.enable_autoimport_completions && ctx.config.resolve_additional_edits_lazily() {
fuzzy_completion(acc, ctx).unwrap_or_default()
}
}
}
}
+// Feature: Fuzzy Completion and Autoimports
+//
+// When completing names in the current scope, proposes additional imports from other modules or crates,
+// if they can be qualified in the scope and their name contains all symbols from the completion input
+// (case-insensitive, in any order or places).
+//
+// ```
+// fn main() {
+// pda<|>
+// }
+// # pub mod std { pub mod marker { pub struct PhantomData { } } }
+// ```
+// ->
+// ```
+// use std::marker::PhantomData;
+//
+// fn main() {
+// PhantomData
+// }
+// # pub mod std { pub mod marker { pub struct PhantomData { } } }
+// ```
+//
+// .Fuzzy search details
+//
+// To avoid an excessive amount of the results returned, completion input is checked for inclusion in the identifiers only
+// (i.e. in `HashMap` in the `std::collections::HashMap` path), also not in the module indentifiers.
+//
+// .Merge Behaviour
+//
+// It is possible to configure how use-trees are merged with the `importMergeBehaviour` setting.
+// Mimics the corresponding behaviour of the `Auto Import` feature.
+//
+// .LSP and performance implications
+//
+// The feature is enabled only if the LSP client supports LSP protocol version 3.16+ and reports the `additionalTextEdits`
+// (case sensitive) resolve client capability in its client capabilities.
+// This way the server is able to defer the costly computations, doing them for a selected completion item only.
+// For clients with no such support, all edits have to be calculated on the completion request, including the fuzzy search completion ones,
+// which might be slow ergo the feature is automatically disabled.
+//
+// .Feature toggle
+//
+// The feature can be forcefully turned off in the settings with the `rust-analyzer.completion.enableAutoimportCompletions` flag.
+// Note that having this flag set to `true` does not guarantee that the feature is enabled: your client needs to have the corredponding
+// capability enabled.
fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
let _p = profile::span("fuzzy_completion");
+ let potential_import_name = ctx.token.to_string();
+
let current_module = ctx.scope.module()?;
let anchor = ctx.name_ref_syntax.as_ref()?;
let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
- let potential_import_name = ctx.token.to_string();
-
let possible_imports = imports_locator::find_similar_imports(
&ctx.sema,
ctx.krate?,
+ Some(100),
&potential_import_name,
- 50,
true,
)
.filter_map(|import_candidate| {
})
})
.filter(|(mod_path, _)| mod_path.len() > 1)
- .take(20)
.filter_map(|(import_path, definition)| {
render_resolution_with_import(
RenderContext::new(ctx),
- import_path.clone(),
- import_scope.clone(),
- ctx.config.merge,
+ ImportEdit {
+ import_path: import_path.clone(),
+ import_scope: import_scope.clone(),
+ merge_behaviour: ctx.config.merge,
+ },
&definition,
)
});
use test_utils::mark;
use crate::{
- test_utils::{check_edit, completion_list},
- CompletionKind,
+ test_utils::{check_edit, check_edit_with_config, completion_list},
+ CompletionConfig, CompletionKind,
};
fn check(ra_fixture: &str, expect: Expect) {
#[test]
fn function_fuzzy_completion() {
- check_edit(
+ let mut completion_config = CompletionConfig::default();
+ completion_config
+ .active_resolve_capabilities
+ .insert(crate::CompletionResolveCapability::AdditionalTextEdits);
+
+ check_edit_with_config(
+ completion_config,
"stdin",
r#"
//- /lib.rs crate:dep
#[test]
fn macro_fuzzy_completion() {
- check_edit(
+ let mut completion_config = CompletionConfig::default();
+ completion_config
+ .active_resolve_capabilities
+ .insert(crate::CompletionResolveCapability::AdditionalTextEdits);
+
+ check_edit_with_config(
+ completion_config,
"macro_with_curlies!",
r#"
//- /lib.rs crate:dep
#[test]
fn struct_fuzzy_completion() {
- check_edit(
+ let mut completion_config = CompletionConfig::default();
+ completion_config
+ .active_resolve_capabilities
+ .insert(crate::CompletionResolveCapability::AdditionalTextEdits);
+
+ check_edit_with_config(
+ completion_config,
"ThirdStruct",
r#"
//- /lib.rs crate:dep
//! completions if we are allowed to.
use ide_db::helpers::insert_use::MergeBehaviour;
+use rustc_hash::FxHashSet;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompletionConfig {
pub enable_postfix_completions: bool,
- pub enable_experimental_completions: bool,
+ pub enable_autoimport_completions: bool,
pub add_call_parenthesis: bool,
pub add_call_argument_snippets: bool,
pub snippet_cap: Option<SnippetCap>,
pub merge: Option<MergeBehaviour>,
+ /// A set of capabilities, enabled on the client and supported on the server.
+ pub active_resolve_capabilities: FxHashSet<CompletionResolveCapability>,
+}
+
+/// A resolve capability, supported on the server.
+/// If the client registers any completion resolve capabilities,
+/// the server is able to render completion items' corresponding fields later,
+/// not during an initial completion item request.
+/// See https://github.com/rust-analyzer/rust-analyzer/issues/6366 for more details.
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
+pub enum CompletionResolveCapability {
+ Documentation,
+ Detail,
+ AdditionalTextEdits,
}
impl CompletionConfig {
pub fn allow_snippets(&mut self, yes: bool) {
self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None }
}
+
+ /// Whether the completions' additional edits are calculated when sending an initional completions list
+ /// or later, in a separate resolve request.
+ pub fn resolve_additional_edits_lazily(&self) -> bool {
+ self.active_resolve_capabilities.contains(&CompletionResolveCapability::AdditionalTextEdits)
+ }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
fn default() -> Self {
CompletionConfig {
enable_postfix_completions: true,
- enable_experimental_completions: true,
+ enable_autoimport_completions: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: Some(SnippetCap { _private: () }),
merge: Some(MergeBehaviour::Full),
+ active_resolve_capabilities: FxHashSet::default(),
}
}
}
/// `CompletionItem` describes a single completion variant in the editor pop-up.
/// It is basically a POD with various properties. To construct a
/// `CompletionItem`, use `new` method and the `Builder` struct.
+#[derive(Clone)]
pub struct CompletionItem {
/// Used only internally in tests, to check only specific kind of
/// completion (postfix, keyword, reference, etc).
/// Indicates that a reference or mutable reference to this variable is a
/// possible match.
ref_match: Option<(Mutability, CompletionScore)>,
+
+ /// The import data to add to completion's edits.
+ import_to_add: Option<ImportEdit>,
}
// We use custom debug for CompletionItem to make snapshot tests more readable.
pub fn ref_match(&self) -> Option<(Mutability, CompletionScore)> {
self.ref_match
}
+
+ pub fn import_to_add(&self) -> Option<&ImportEdit> {
+ self.import_to_add.as_ref()
+ }
}
/// An extra import to add after the completion is applied.
-#[derive(Clone)]
-pub(crate) struct ImportToAdd {
- pub(crate) import_path: ModPath,
- pub(crate) import_scope: ImportScope,
- pub(crate) merge_behaviour: Option<MergeBehaviour>,
+#[derive(Debug, Clone)]
+pub struct ImportEdit {
+ pub import_path: ModPath,
+ pub import_scope: ImportScope,
+ pub merge_behaviour: Option<MergeBehaviour>,
+}
+
+impl ImportEdit {
+ /// Attempts to insert the import to the given scope, producing a text edit.
+ /// May return no edit in edge cases, such as scope already containing the import.
+ pub fn to_text_edit(&self) -> Option<TextEdit> {
+ let _p = profile::span("ImportEdit::to_text_edit");
+
+ let rewriter = insert_use::insert_use(
+ &self.import_scope,
+ mod_path_to_ast(&self.import_path),
+ self.merge_behaviour,
+ );
+ let old_ast = rewriter.rewrite_root()?;
+ let mut import_insert = TextEdit::builder();
+ algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut import_insert);
+
+ Some(import_insert.finish())
+ }
}
/// A helper to make `CompletionItem`s.
pub(crate) struct Builder {
source_range: TextRange,
completion_kind: CompletionKind,
- import_to_add: Option<ImportToAdd>,
+ import_to_add: Option<ImportEdit>,
label: String,
insert_text: Option<String>,
insert_text_format: InsertTextFormat,
let mut label = self.label;
let mut lookup = self.lookup;
let mut insert_text = self.insert_text;
- let mut text_edits = TextEdit::builder();
- if let Some(import_data) = self.import_to_add {
- let import = mod_path_to_ast(&import_data.import_path);
- let mut import_path_without_last_segment = import_data.import_path;
+ if let Some(import_to_add) = self.import_to_add.as_ref() {
+ let mut import_path_without_last_segment = import_to_add.import_path.to_owned();
let _ = import_path_without_last_segment.segments.pop();
if !import_path_without_last_segment.segments.is_empty() {
}
label = format!("{}::{}", import_path_without_last_segment, label);
}
-
- let rewriter = insert_use::insert_use(
- &import_data.import_scope,
- import,
- import_data.merge_behaviour,
- );
- if let Some(old_ast) = rewriter.rewrite_root() {
- algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut text_edits);
- }
}
- let original_edit = match self.text_edit {
+ let text_edit = match self.text_edit {
Some(it) => it,
None => {
TextEdit::replace(self.source_range, insert_text.unwrap_or_else(|| label.clone()))
}
};
- let mut resulting_edit = text_edits.finish();
- resulting_edit.union(original_edit).expect("Failed to unite text edits");
-
CompletionItem {
source_range: self.source_range,
label,
insert_text_format: self.insert_text_format,
- text_edit: resulting_edit,
+ text_edit,
detail: self.detail,
documentation: self.documentation,
lookup,
trigger_call_info: self.trigger_call_info.unwrap_or(false),
score: self.score,
ref_match: self.ref_match,
+ import_to_add: self.import_to_add,
}
}
pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder {
self.trigger_call_info = Some(true);
self
}
- pub(crate) fn add_import(mut self, import_to_add: Option<ImportToAdd>) -> Builder {
+ pub(crate) fn add_import(mut self, import_to_add: Option<ImportEdit>) -> Builder {
self.import_to_add = import_to_add;
self
}
mod completions;
-use ide_db::base_db::FilePosition;
-use ide_db::RootDatabase;
+use ide_db::{
+ base_db::FilePosition, helpers::insert_use::ImportScope, imports_locator, RootDatabase,
+};
+use syntax::AstNode;
+use text_edit::TextEdit;
use crate::{completions::Completions, context::CompletionContext, item::CompletionKind};
pub use crate::{
- config::CompletionConfig,
- item::{CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat},
+ config::{CompletionConfig, CompletionResolveCapability},
+ item::{CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, InsertTextFormat},
};
//FIXME: split the following feature into fine-grained features.
// }
// ```
//
-// And experimental completions, enabled with the `rust-analyzer.completion.enableExperimental` setting.
-// This flag enables or disables:
-//
-// - Auto import: additional completion options with automatic `use` import and options from all project importable items, matched for the input
-//
-// Experimental completions might cause issues with performance and completion list look.
+// And the auto import completions, enabled with the `rust-analyzer.completion.autoimport.enable` setting and the corresponding LSP client capabilities.
+// Those are the additional completion options with automatic `use` import and options from all project importable items,
+// fuzzy matched agains the completion imput.
/// Main entry point for completion. We run completion as a two-phase process.
///
Some(acc)
}
+/// Resolves additional completion data at the position given.
+pub fn resolve_completion_edits(
+ db: &RootDatabase,
+ config: &CompletionConfig,
+ position: FilePosition,
+ full_import_path: &str,
+ imported_name: &str,
+) -> Option<Vec<TextEdit>> {
+ let ctx = CompletionContext::new(db, position, config)?;
+ let anchor = ctx.name_ref_syntax.as_ref()?;
+ let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
+
+ let current_module = ctx.sema.scope(anchor.syntax()).module()?;
+ let current_crate = current_module.krate();
+
+ let import_path = imports_locator::find_exact_imports(&ctx.sema, current_crate, imported_name)
+ .filter_map(|candidate| {
+ let item: hir::ItemInNs = candidate.either(Into::into, Into::into);
+ current_module.find_use_path(db, item)
+ })
+ .find(|mod_path| mod_path.to_string() == full_import_path)?;
+
+ ImportEdit { import_path, import_scope, merge_behaviour: config.merge }
+ .to_text_edit()
+ .map(|edit| vec![edit])
+}
+
#[cfg(test)]
mod tests {
use crate::config::CompletionConfig;
mod builder_ext;
-use hir::{Documentation, HasAttrs, HirDisplay, ModPath, Mutability, ScopeDef, Type};
-use ide_db::helpers::insert_use::{ImportScope, MergeBehaviour};
+use hir::{Documentation, HasAttrs, HirDisplay, Mutability, ScopeDef, Type};
use ide_db::RootDatabase;
use syntax::TextRange;
use test_utils::mark;
use crate::{
- config::SnippetCap, item::ImportToAdd, CompletionContext, CompletionItem, CompletionItemKind,
+ config::SnippetCap, item::ImportEdit, CompletionContext, CompletionItem, CompletionItemKind,
CompletionKind, CompletionScore,
};
pub(crate) fn render_resolution_with_import<'a>(
ctx: RenderContext<'a>,
- import_path: ModPath,
- import_scope: ImportScope,
- merge_behaviour: Option<MergeBehaviour>,
+ import_edit: ImportEdit,
resolution: &ScopeDef,
) -> Option<CompletionItem> {
- let local_name = import_path.segments.last()?.to_string();
Render::new(ctx).render_resolution(
- local_name,
- Some(ImportToAdd { import_path, import_scope, merge_behaviour }),
+ import_edit.import_path.segments.last()?.to_string(),
+ Some(import_edit),
resolution,
)
}
fn render_resolution(
self,
local_name: String,
- import_to_add: Option<ImportToAdd>,
+ import_to_add: Option<ImportEdit>,
resolution: &ScopeDef,
) -> Option<CompletionItem> {
let _p = profile::span("render_resolution");
insert: "m",
kind: Module,
},
- CompletionItem {
- label: "m::Spam",
- source_range: 75..76,
- text_edit: TextEdit {
- indels: [
- Indel {
- insert: "use m::Spam;",
- delete: 0..0,
- },
- Indel {
- insert: "\n\n",
- delete: 0..0,
- },
- Indel {
- insert: "Spam",
- delete: 75..76,
- },
- ],
- },
- kind: Enum,
- lookup: "Spam",
- },
CompletionItem {
label: "m::Spam::Foo",
source_range: 75..76,
use test_utils::mark;
use crate::{
- item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
+ item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
render::{builder_ext::Params, RenderContext},
};
pub(crate) fn render_enum_variant<'a>(
ctx: RenderContext<'a>,
- import_to_add: Option<ImportToAdd>,
+ import_to_add: Option<ImportEdit>,
local_name: Option<String>,
variant: hir::EnumVariant,
path: Option<ModPath>,
}
}
- fn render(self, import_to_add: Option<ImportToAdd>) -> CompletionItem {
+ fn render(self, import_to_add: Option<ImportEdit>) -> CompletionItem {
let mut builder = CompletionItem::new(
CompletionKind::Reference,
self.ctx.source_range(),
use test_utils::mark;
use crate::{
- item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
+ item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
render::{builder_ext::Params, RenderContext},
};
pub(crate) fn render_fn<'a>(
ctx: RenderContext<'a>,
- import_to_add: Option<ImportToAdd>,
+ import_to_add: Option<ImportEdit>,
local_name: Option<String>,
fn_: hir::Function,
) -> CompletionItem {
FunctionRender { ctx, name, func: fn_, ast_node }
}
- fn render(self, import_to_add: Option<ImportToAdd>) -> CompletionItem {
+ fn render(self, import_to_add: Option<ImportEdit>) -> CompletionItem {
let params = self.params();
CompletionItem::new(CompletionKind::Reference, self.ctx.source_range(), self.name.clone())
.kind(self.kind())
use test_utils::mark;
use crate::{
- item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
+ item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
render::RenderContext,
};
pub(crate) fn render_macro<'a>(
ctx: RenderContext<'a>,
- import_to_add: Option<ImportToAdd>,
+ import_to_add: Option<ImportEdit>,
name: String,
macro_: hir::MacroDef,
) -> Option<CompletionItem> {
MacroRender { ctx, name, macro_, docs, bra, ket }
}
- fn render(&self, import_to_add: Option<ImportToAdd>) -> Option<CompletionItem> {
+ fn render(&self, import_to_add: Option<ImportEdit>) -> Option<CompletionItem> {
// FIXME: Currently proc-macro do not have ast-node,
// such that it does not have source
if self.macro_.is_proc_macro() {
.collect_tuple()
.unwrap_or_else(|| panic!("can't find {:?} completion in {:#?}", what, completions));
let mut actual = db.file_text(position.file_id).to_string();
- completion.text_edit().apply(&mut actual);
+
+ let mut combined_edit = completion.text_edit().to_owned();
+ if let Some(import_text_edit) = completion.import_to_add().and_then(|edit| edit.to_text_edit())
+ {
+ combined_edit.union(import_text_edit).expect(
+ "Failed to apply completion resolve changes: change ranges overlap, but should not",
+ )
+ }
+
+ combined_edit.apply(&mut actual);
assert_eq_text!(&ra_fixture_after, &actual)
}
},
};
pub use completion::{
- CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat,
+ CompletionConfig, CompletionItem, CompletionItemKind, CompletionResolveCapability,
+ CompletionScore, ImportEdit, InsertTextFormat,
};
pub use ide_db::{
call_info::CallInfo,
self.with_db(|db| completion::completions(db, config, position).map(Into::into))
}
+ /// Resolves additional completion data at the position given.
+ pub fn resolve_completion_edits(
+ &self,
+ config: &CompletionConfig,
+ position: FilePosition,
+ full_import_path: &str,
+ imported_name: &str,
+ ) -> Cancelable<Vec<TextEdit>> {
+ Ok(self
+ .with_db(|db| {
+ completion::resolve_completion_edits(
+ db,
+ config,
+ position,
+ full_import_path,
+ imported_name,
+ )
+ })?
+ .unwrap_or_default())
+ }
+
/// Computes resolved assists with source changes for the given position.
pub fn resolved_assists(
&self,
pub fn find_similar_imports<'a>(
sema: &Semantics<'a, RootDatabase>,
krate: Crate,
+ limit: Option<usize>,
name_to_import: &str,
- limit: usize,
ignore_modules: bool,
) -> impl Iterator<Item = Either<ModuleDef, MacroDef>> {
let _p = profile::span("find_similar_imports");
- let mut external_query = import_map::Query::new(name_to_import).limit(limit);
+ let mut external_query = import_map::Query::new(name_to_import);
if ignore_modules {
external_query = external_query.exclude_import_kind(import_map::ImportKind::Module);
}
- find_imports(
- sema,
- krate,
- {
- let mut local_query = symbol_index::Query::new(name_to_import.to_string());
- local_query.limit(limit);
- local_query
- },
- external_query,
- )
+ let mut local_query = symbol_index::Query::new(name_to_import.to_string());
+
+ if let Some(limit) = limit {
+ local_query.limit(limit);
+ external_query = external_query.limit(limit);
+ }
+
+ find_imports(sema, krate, local_query, external_query)
}
fn find_imports<'a>(
//! Advertizes the capabilities of the LSP Server.
use std::env;
+use ide::CompletionResolveCapability;
use lsp_types::{
CallHierarchyServerCapability, ClientCapabilities, CodeActionKind, CodeActionOptions,
CodeActionProviderCapability, CodeLensOptions, CompletionOptions,
TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability,
WorkDoneProgressOptions,
};
+use rustc_hash::FxHashSet;
use serde_json::json;
use crate::semantic_tokens;
})),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
- resolve_provider: None,
+ resolve_provider: completions_resolve_provider(client_caps),
trigger_characters: Some(vec![":".to_string(), ".".to_string()]),
work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
}),
}
}
+fn completions_resolve_provider(client_caps: &ClientCapabilities) -> Option<bool> {
+ if enabled_completions_resolve_capabilities(client_caps)?.is_empty() {
+ log::info!("No `additionalTextEdits` completion resolve capability was found in the client capabilities, autoimport completion is disabled");
+ None
+ } else {
+ Some(true)
+ }
+}
+
+/// Parses client capabilities and returns all completion resolve capabilities rust-analyzer supports.
+pub(crate) fn enabled_completions_resolve_capabilities(
+ caps: &ClientCapabilities,
+) -> Option<FxHashSet<CompletionResolveCapability>> {
+ Some(
+ caps.text_document
+ .as_ref()?
+ .completion
+ .as_ref()?
+ .completion_item
+ .as_ref()?
+ .resolve_support
+ .as_ref()?
+ .properties
+ .iter()
+ .filter_map(|cap_string| match cap_string.as_str() {
+ "additionalTextEdits" => Some(CompletionResolveCapability::AdditionalTextEdits),
+ "detail" => Some(CompletionResolveCapability::Detail),
+ "documentation" => Some(CompletionResolveCapability::Documentation),
+ _unsupported => None,
+ })
+ .collect(),
+ )
+}
+
fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProviderCapability {
client_caps
.text_document
use serde::Deserialize;
use vfs::AbsPathBuf;
-use crate::diagnostics::DiagnosticsMapConfig;
+use crate::{caps::enabled_completions_resolve_capabilities, diagnostics::DiagnosticsMapConfig};
#[derive(Debug, Clone)]
pub struct Config {
},
completion: CompletionConfig {
enable_postfix_completions: true,
- enable_experimental_completions: true,
+ enable_autoimport_completions: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
..CompletionConfig::default()
};
self.completion.enable_postfix_completions = data.completion_postfix_enable;
- self.completion.enable_experimental_completions = data.completion_enableExperimental;
+ self.completion.enable_autoimport_completions = data.completion_autoimport_enable;
self.completion.add_call_parenthesis = data.completion_addCallParenthesis;
self.completion.add_call_argument_snippets = data.completion_addCallArgumentSnippets;
self.completion.merge = self.assist.insert_use.merge;
}
self.completion.allow_snippets(false);
+ self.completion.active_resolve_capabilities =
+ enabled_completions_resolve_capabilities(caps).unwrap_or_default();
if let Some(completion) = &doc_caps.completion {
if let Some(completion_item) = &completion.completion_item {
if let Some(value) = completion_item.snippet_support {
completion_addCallArgumentSnippets: bool = true,
completion_addCallParenthesis: bool = true,
completion_postfix_enable: bool = true,
- completion_enableExperimental: bool = true,
+ completion_autoimport_enable: bool = true,
diagnostics_enable: bool = true,
diagnostics_enableExperimental: bool = true,
};
use ide::{
- FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData, NavigationTarget, Query,
- RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
+ CompletionResolveCapability, FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData,
+ NavigationTarget, Query, RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
};
use itertools::Itertools;
use lsp_server::ErrorCode;
HoverContents, Location, NumberOrString, Position, PrepareRenameResponse, Range, RenameParams,
SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, SemanticTokensParams,
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
- SymbolTag, TextDocumentIdentifier, Url, WorkspaceEdit,
+ SymbolTag, TextDocumentIdentifier, TextDocumentPositionParams, Url, WorkspaceEdit,
};
use project_model::TargetKind;
use serde::{Deserialize, Serialize};
from_json, from_proto,
global_state::{GlobalState, GlobalStateSnapshot},
lsp_ext::{self, InlayHint, InlayHintsParams},
+ lsp_utils::all_edits_are_disjoint,
to_proto, LspError, Result,
};
params: lsp_types::CompletionParams,
) -> Result<Option<lsp_types::CompletionResponse>> {
let _p = profile::span("handle_completion");
+ let text_document_position = params.text_document_position.clone();
let position = from_proto::file_position(&snap, params.text_document_position)?;
let completion_triggered_after_single_colon = {
let mut res = false;
};
let line_index = snap.analysis.file_line_index(position.file_id)?;
let line_endings = snap.file_line_endings(position.file_id);
+
let items: Vec<CompletionItem> = items
.into_iter()
- .flat_map(|item| to_proto::completion_item(&line_index, line_endings, item))
+ .flat_map(|item| {
+ let mut new_completion_items =
+ to_proto::completion_item(&line_index, line_endings, item.clone());
+
+ if snap.config.completion.resolve_additional_edits_lazily() {
+ for new_item in &mut new_completion_items {
+ let _ = fill_resolve_data(&mut new_item.data, &item, &text_document_position)
+ .take();
+ }
+ }
+
+ new_completion_items
+ })
.collect();
let completion_list = lsp_types::CompletionList { is_incomplete: true, items };
Ok(Some(completion_list.into()))
}
+pub(crate) fn handle_completion_resolve(
+ snap: GlobalStateSnapshot,
+ mut original_completion: CompletionItem,
+) -> Result<CompletionItem> {
+ let _p = profile::span("handle_completion_resolve");
+
+ if !all_edits_are_disjoint(&original_completion, &[]) {
+ return Err(LspError::new(
+ ErrorCode::InvalidParams as i32,
+ "Received a completion with overlapping edits, this is not LSP-compliant".into(),
+ )
+ .into());
+ }
+
+ // FIXME resolve the other capabilities also?
+ if !snap
+ .config
+ .completion
+ .active_resolve_capabilities
+ .contains(&CompletionResolveCapability::AdditionalTextEdits)
+ {
+ return Ok(original_completion);
+ }
+
+ let resolve_data = match original_completion
+ .data
+ .take()
+ .map(|data| serde_json::from_value::<CompletionResolveData>(data))
+ .transpose()?
+ {
+ Some(data) => data,
+ None => return Ok(original_completion),
+ };
+
+ let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?;
+ let line_index = snap.analysis.file_line_index(file_id)?;
+ let line_endings = snap.file_line_endings(file_id);
+ let offset = from_proto::offset(&line_index, resolve_data.position.position);
+
+ let additional_edits = snap
+ .analysis
+ .resolve_completion_edits(
+ &snap.config.completion,
+ FilePosition { file_id, offset },
+ &resolve_data.full_import_path,
+ &resolve_data.imported_name,
+ )?
+ .into_iter()
+ .flat_map(|edit| {
+ edit.into_iter().map(|indel| to_proto::text_edit(&line_index, line_endings, indel))
+ })
+ .collect_vec();
+
+ if !all_edits_are_disjoint(&original_completion, &additional_edits) {
+ return Err(LspError::new(
+ ErrorCode::InternalError as i32,
+ "Import edit overlaps with the original completion edits, this is not LSP-compliant"
+ .into(),
+ )
+ .into());
+ }
+
+ if let Some(original_additional_edits) = original_completion.additional_text_edits.as_mut() {
+ original_additional_edits.extend(additional_edits.into_iter())
+ } else {
+ original_completion.additional_text_edits = Some(additional_edits);
+ }
+
+ Ok(original_completion)
+}
+
pub(crate) fn handle_folding_range(
snap: GlobalStateSnapshot,
params: FoldingRangeParams,
_ => false,
}
}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct CompletionResolveData {
+ position: lsp_types::TextDocumentPositionParams,
+ full_import_path: String,
+ imported_name: String,
+}
+
+fn fill_resolve_data(
+ resolve_data: &mut Option<serde_json::Value>,
+ item: &ide::CompletionItem,
+ position: &TextDocumentPositionParams,
+) -> Option<()> {
+ let import_edit = item.import_to_add()?;
+ let full_import_path = import_edit.import_path.to_string();
+ let imported_name = import_edit.import_path.segments.clone().pop()?.to_string();
+
+ *resolve_data = Some(
+ to_value(CompletionResolveData {
+ position: position.to_owned(),
+ full_import_path,
+ imported_name,
+ })
+ .unwrap(),
+ );
+ Some(())
+}
}
}
+/// Checks that the edits inside the completion and the additional edits do not overlap.
+/// LSP explicitly forbits the additional edits to overlap both with the main edit and themselves.
+pub(crate) fn all_edits_are_disjoint(
+ completion: &lsp_types::CompletionItem,
+ additional_edits: &[lsp_types::TextEdit],
+) -> bool {
+ let mut edit_ranges = Vec::new();
+ match completion.text_edit.as_ref() {
+ Some(lsp_types::CompletionTextEdit::Edit(edit)) => {
+ edit_ranges.push(edit.range);
+ }
+ Some(lsp_types::CompletionTextEdit::InsertAndReplace(edit)) => {
+ edit_ranges.push(edit.insert);
+ edit_ranges.push(edit.replace);
+ }
+ None => {}
+ }
+ if let Some(additional_changes) = completion.additional_text_edits.as_ref() {
+ edit_ranges.extend(additional_changes.iter().map(|edit| edit.range));
+ };
+ edit_ranges.extend(additional_edits.iter().map(|edit| edit.range));
+ edit_ranges.sort_by_key(|range| (range.start, range.end));
+ edit_ranges
+ .iter()
+ .zip(edit_ranges.iter().skip(1))
+ .all(|(previous, next)| previous.end <= next.start)
+}
+
#[cfg(test)]
mod tests {
- use lsp_types::{Position, Range, TextDocumentContentChangeEvent};
+ use lsp_types::{
+ CompletionItem, CompletionTextEdit, InsertReplaceEdit, Position, Range,
+ TextDocumentContentChangeEvent,
+ };
use super::*;
apply_document_changes(&mut text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]);
assert_eq!(text, "ațc\ncb");
}
+
+ #[test]
+ fn empty_completion_disjoint_tests() {
+ let empty_completion =
+ CompletionItem::new_simple("label".to_string(), "detail".to_string());
+
+ let disjoint_edit_1 = lsp_types::TextEdit::new(
+ Range::new(Position::new(2, 2), Position::new(3, 3)),
+ "new_text".to_string(),
+ );
+ let disjoint_edit_2 = lsp_types::TextEdit::new(
+ Range::new(Position::new(3, 3), Position::new(4, 4)),
+ "new_text".to_string(),
+ );
+
+ let joint_edit = lsp_types::TextEdit::new(
+ Range::new(Position::new(1, 1), Position::new(5, 5)),
+ "new_text".to_string(),
+ );
+
+ assert!(
+ all_edits_are_disjoint(&empty_completion, &[]),
+ "Empty completion has all its edits disjoint"
+ );
+ assert!(
+ all_edits_are_disjoint(
+ &empty_completion,
+ &[disjoint_edit_1.clone(), disjoint_edit_2.clone()]
+ ),
+ "Empty completion is disjoint to whatever disjoint extra edits added"
+ );
+
+ assert!(
+ !all_edits_are_disjoint(
+ &empty_completion,
+ &[disjoint_edit_1, disjoint_edit_2, joint_edit]
+ ),
+ "Empty completion does not prevent joint extra edits from failing the validation"
+ );
+ }
+
+ #[test]
+ fn completion_with_joint_edits_disjoint_tests() {
+ let disjoint_edit = lsp_types::TextEdit::new(
+ Range::new(Position::new(1, 1), Position::new(2, 2)),
+ "new_text".to_string(),
+ );
+ let disjoint_edit_2 = lsp_types::TextEdit::new(
+ Range::new(Position::new(2, 2), Position::new(3, 3)),
+ "new_text".to_string(),
+ );
+ let joint_edit = lsp_types::TextEdit::new(
+ Range::new(Position::new(1, 1), Position::new(5, 5)),
+ "new_text".to_string(),
+ );
+
+ let mut completion_with_joint_edits =
+ CompletionItem::new_simple("label".to_string(), "detail".to_string());
+ completion_with_joint_edits.additional_text_edits =
+ Some(vec![disjoint_edit.clone(), joint_edit.clone()]);
+ assert!(
+ !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+ "Completion with disjoint edits fails the validaton even with empty extra edits"
+ );
+
+ completion_with_joint_edits.text_edit =
+ Some(CompletionTextEdit::Edit(disjoint_edit.clone()));
+ completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit.clone()]);
+ assert!(
+ !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+ "Completion with disjoint edits fails the validaton even with empty extra edits"
+ );
+
+ completion_with_joint_edits.text_edit =
+ Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
+ new_text: "new_text".to_string(),
+ insert: disjoint_edit.range,
+ replace: joint_edit.range,
+ }));
+ completion_with_joint_edits.additional_text_edits = None;
+ assert!(
+ !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+ "Completion with disjoint edits fails the validaton even with empty extra edits"
+ );
+
+ completion_with_joint_edits.text_edit =
+ Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
+ new_text: "new_text".to_string(),
+ insert: disjoint_edit.range,
+ replace: disjoint_edit_2.range,
+ }));
+ completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit]);
+ assert!(
+ !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+ "Completion with disjoint edits fails the validaton even with empty extra edits"
+ );
+ }
+
+ #[test]
+ fn completion_with_disjoint_edits_disjoint_tests() {
+ let disjoint_edit = lsp_types::TextEdit::new(
+ Range::new(Position::new(1, 1), Position::new(2, 2)),
+ "new_text".to_string(),
+ );
+ let disjoint_edit_2 = lsp_types::TextEdit::new(
+ Range::new(Position::new(2, 2), Position::new(3, 3)),
+ "new_text".to_string(),
+ );
+ let joint_edit = lsp_types::TextEdit::new(
+ Range::new(Position::new(1, 1), Position::new(5, 5)),
+ "new_text".to_string(),
+ );
+
+ let mut completion_with_disjoint_edits =
+ CompletionItem::new_simple("label".to_string(), "detail".to_string());
+ completion_with_disjoint_edits.text_edit = Some(CompletionTextEdit::Edit(disjoint_edit));
+ let completion_with_disjoint_edits = completion_with_disjoint_edits;
+
+ assert!(
+ all_edits_are_disjoint(&completion_with_disjoint_edits, &[]),
+ "Completion with disjoint edits is valid"
+ );
+ assert!(
+ !all_edits_are_disjoint(&completion_with_disjoint_edits, &[joint_edit.clone()]),
+ "Completion with disjoint edits and joint extra edit is invalid"
+ );
+ assert!(
+ all_edits_are_disjoint(&completion_with_disjoint_edits, &[disjoint_edit_2.clone()]),
+ "Completion with disjoint edits and joint extra edit is valid"
+ );
+ }
}
.on::<lsp_types::request::GotoImplementation>(handlers::handle_goto_implementation)
.on::<lsp_types::request::GotoTypeDefinition>(handlers::handle_goto_type_definition)
.on::<lsp_types::request::Completion>(handlers::handle_completion)
+ .on::<lsp_types::request::ResolveCompletionItem>(handlers::handle_completion_resolve)
.on::<lsp_types::request::CodeLensRequest>(handlers::handle_code_lens)
.on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)
.on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)
"default": true,
"markdownDescription": "Whether to show postfix snippets like `dbg`, `if`, `not`, etc."
},
- "rust-analyzer.completion.enableExperimental": {
+ "rust-analyzer.completion.autoimport.enable": {
"type": "boolean",
"default": true,
- "markdownDescription": "Display additional completions with potential false positives and performance issues"
+ "markdownDescription": [
+ "Toggles the additional completions that automatically add imports when completed.",
+ "Note that your client have to specify the `additionalTextEdits` LSP client capability to truly have this feature enabled"
+ ]
},
"rust-analyzer.callInfo.full": {
"type": "boolean",