From 62d97d9ba7c41b3670e8c77f0fa6735162a3bbf8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Dec 2024 19:53:30 +0200 Subject: [PATCH] Draft completion hashing --- Cargo.lock | 8 ++ crates/ide-completion/src/item.rs | 3 +- crates/ide-completion/src/lib.rs | 1 + crates/rust-analyzer/Cargo.toml | 16 ++-- crates/rust-analyzer/src/handlers/request.rs | 26 ++++-- crates/rust-analyzer/src/lib.rs | 89 ++++++++++++++++++++ crates/rust-analyzer/src/lsp/ext.rs | 3 +- crates/rust-analyzer/src/lsp/to_proto.rs | 18 +++- docs/dev/lsp-extensions.md | 2 +- 9 files changed, 142 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5861833d53..b6f2c6faf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1659,6 +1659,7 @@ dependencies = [ "hir-def", "hir-ty", "ide", + "ide-completion", "ide-db", "ide-ssr", "intern", @@ -1687,6 +1688,7 @@ dependencies = [ "stdx", "syntax", "syntax-bridge", + "tenthash", "test-fixture", "test-utils", "tikv-jemallocator", @@ -1990,6 +1992,12 @@ dependencies = [ "tt", ] +[[package]] +name = "tenthash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d67f9f3cf70e0852941d7bc3cb884b49b24b8ee956baf91ad0abae31f5ef11fb" + [[package]] name = "test-fixture" version = "0.0.0" diff --git a/crates/ide-completion/src/item.rs b/crates/ide-completion/src/item.rs index 52f6bedaaa..8878fbbea3 100644 --- a/crates/ide-completion/src/item.rs +++ b/crates/ide-completion/src/item.rs @@ -346,8 +346,7 @@ pub enum CompletionItemKind { impl_from!(SymbolKind for CompletionItemKind); impl CompletionItemKind { - #[cfg(test)] - pub(crate) fn tag(self) -> &'static str { + pub fn tag(self) -> &'static str { match self { CompletionItemKind::SymbolKind(kind) => match kind { SymbolKind::Attribute => "at", diff --git a/crates/ide-completion/src/lib.rs b/crates/ide-completion/src/lib.rs index cffdfa29f1..14f42b4005 100644 --- a/crates/ide-completion/src/lib.rs +++ b/crates/ide-completion/src/lib.rs @@ -34,6 +34,7 @@ pub use crate::{ config::{CallableSnippets, CompletionConfig}, item::{ CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevancePostfixMatch, + CompletionRelevanceReturnType, CompletionRelevanceTypeMatch, }, snippet::{Snippet, SnippetScope}, }; diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index 2dd2f2242a..022b0a0ecf 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml @@ -24,6 +24,7 @@ anyhow.workspace = true crossbeam-channel.workspace = true dirs = "5.0.1" dissimilar.workspace = true +ide-completion.workspace = true itertools.workspace = true scip = "0.5.1" lsp-types = { version = "=0.95.0", features = ["proposed"] } @@ -34,6 +35,7 @@ rayon.workspace = true rustc-hash.workspace = true serde_json = { workspace = true, features = ["preserve_order"] } serde.workspace = true +tenthash = "0.4.0" num_cpus = "1.15.0" mimalloc = { version = "0.1.30", default-features = false, optional = true } lsp-server.workspace = true @@ -90,13 +92,13 @@ jemalloc = ["jemallocator", "profile/jemalloc"] force-always-assert = ["always-assert/force"] sysroot-abi = [] in-rust-tree = [ - "sysroot-abi", - "syntax/in-rust-tree", - "parser/in-rust-tree", - "hir/in-rust-tree", - "hir-def/in-rust-tree", - "hir-ty/in-rust-tree", - "load-cargo/in-rust-tree", + "sysroot-abi", + "syntax/in-rust-tree", + "parser/in-rust-tree", + "hir/in-rust-tree", + "hir-def/in-rust-tree", + "hir-ty/in-rust-tree", + "load-cargo/in-rust-tree", ] [lints] diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 0fadfa6c42..9dd6dc999b 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -36,6 +36,7 @@ use triomphe::Arc; use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use crate::{ + completion_item_hash, config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, diagnostics::convert_diagnostic, global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot}, @@ -1122,12 +1123,15 @@ pub(crate) fn handle_completion_resolve( return Ok(original_completion); }; let source_root = snap.analysis.source_root_id(file_id)?; + let Some(completion_hash_for_resolve) = &resolve_data.completion_item_hash else { + return Ok(original_completion); + }; let mut forced_resolve_completions_config = snap.config.completion(Some(source_root)); forced_resolve_completions_config.fields_to_resolve = CompletionFieldsToResolve::empty(); let position = FilePosition { file_id, offset }; - let Some(resolved_completions) = snap.analysis.completions( + let Some(completions) = snap.analysis.completions( &forced_resolve_completions_config, position, resolve_data.trigger_character, @@ -1135,6 +1139,14 @@ pub(crate) fn handle_completion_resolve( else { return Ok(original_completion); }; + + let Some(corresponding_completion) = completions.into_iter().find(|completion_item| { + let hash = completion_item_hash(&completion_item, resolve_data.for_ref); + &hash == completion_hash_for_resolve + }) else { + return Ok(original_completion); + }; + let mut resolved_completions = to_proto::completion_items( &snap.config, &forced_resolve_completions_config.fields_to_resolve, @@ -1142,15 +1154,11 @@ pub(crate) fn handle_completion_resolve( snap.file_version(position.file_id), resolve_data.position, resolve_data.trigger_character, - resolved_completions, + vec![corresponding_completion], ); - - let mut resolved_completion = - if resolved_completions.get(resolve_data.completion_item_index).is_some() { - resolved_completions.swap_remove(resolve_data.completion_item_index) - } else { - return Ok(original_completion); - }; + let Some(mut resolved_completion) = resolved_completions.pop() else { + return Ok(original_completion); + }; if !resolve_data.imports.is_empty() { let additional_edits = snap diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 234204695c..8f74e75d3d 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -47,7 +47,9 @@ use self::lsp::ext as lsp_ext; #[cfg(test)] mod integrated_benchmarks; +use ide::{CompletionItem, CompletionRelevance, TextEdit, TextRange}; use serde::de::DeserializeOwned; +use tenthash::TentHasher; pub use crate::{ lsp::capabilities::server_capabilities, main_loop::main_loop, reload::ws_to_crate_graph, @@ -61,3 +63,90 @@ pub fn from_json( serde_json::from_value(json.clone()) .map_err(|e| anyhow::format_err!("Failed to deserialize {what}: {e}; {json}")) } + +fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8; 20] { + fn hash_text_range(hasher: &mut TentHasher, text_range: &TextRange) { + hasher.update(u32::from(text_range.start()).to_le_bytes()); + hasher.update(u32::from(text_range.end()).to_le_bytes()); + } + + fn hash_text_edit(hasher: &mut TentHasher, edit: &TextEdit) { + for indel in edit.iter() { + hasher.update(&indel.insert); + hash_text_range(hasher, &indel.delete); + } + } + + fn has_completion_relevance(hasher: &mut TentHasher, relevance: &CompletionRelevance) { + use ide_completion::{ + CompletionRelevancePostfixMatch, CompletionRelevanceReturnType, + CompletionRelevanceTypeMatch, + }; + + if let Some(type_match) = &relevance.type_match { + let label = match type_match { + CompletionRelevanceTypeMatch::CouldUnify => "could_unify", + CompletionRelevanceTypeMatch::Exact => "exact", + }; + hasher.update(label); + } + hasher.update(&[u8::from(relevance.exact_name_match), u8::from(relevance.is_local)]); + if let Some(trait_) = &relevance.trait_ { + hasher.update(&[u8::from(trait_.is_op_method), u8::from(trait_.notable_trait)]); + } + hasher.update(&[ + u8::from(relevance.is_name_already_imported), + u8::from(relevance.requires_import), + u8::from(relevance.is_private_editable), + ]); + if let Some(postfix_match) = &relevance.postfix_match { + let label = match postfix_match { + CompletionRelevancePostfixMatch::NonExact => "non_exact", + CompletionRelevancePostfixMatch::Exact => "exact", + }; + hasher.update(label); + } + if let Some(function) = &relevance.function { + hasher.update(&[u8::from(function.has_params), u8::from(function.has_self_param)]); + let label = match function.return_type { + CompletionRelevanceReturnType::Other => "other", + CompletionRelevanceReturnType::DirectConstructor => "direct_constructor", + CompletionRelevanceReturnType::Constructor => "constructor", + CompletionRelevanceReturnType::Builder => "builder", + }; + hasher.update(label); + } + } + + let mut hasher = TentHasher::new(); + hasher.update(&[ + u8::from(is_ref_completion), + u8::from(item.is_snippet), + u8::from(item.deprecated), + u8::from(item.trigger_call_info), + ]); + hasher.update(&item.label); + if let Some(label_detail) = &item.label_detail { + hasher.update(label_detail); + } + hash_text_range(&mut hasher, &item.source_range); + hash_text_edit(&mut hasher, &item.text_edit); + hasher.update(item.kind.tag()); + hasher.update(&item.lookup); + if let Some(detail) = &item.detail { + hasher.update(detail); + } + if let Some(documentation) = &item.documentation { + hasher.update(documentation.as_str()); + } + has_completion_relevance(&mut hasher, &item.relevance); + if let Some((mutability, text_size)) = &item.ref_match { + hasher.update(mutability.as_keyword_for_ref()); + hasher.update(u32::from(*text_size).to_le_bytes()); + } + for (import_path, import_name) in &item.import_to_add { + hasher.update(import_path); + hasher.update(import_name); + } + hasher.finalize() +} diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs index 6ddfe118d5..7d60ae703b 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -826,7 +826,8 @@ pub struct CompletionResolveData { pub imports: Vec, pub version: Option, pub trigger_character: Option, - pub completion_item_index: usize, + pub for_ref: bool, + pub completion_item_hash: Option<[u8; 20]>, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index d444f90a13..97caed8f08 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -21,6 +21,7 @@ use serde_json::to_value; use vfs::AbsPath; use crate::{ + completion_item_hash, config::{CallInfoConfig, Config}, global_state::GlobalStateSnapshot, line_index::{LineEndings, LineIndex, PositionEncoding}, @@ -274,6 +275,11 @@ fn completion_item( completion_trigger_character: Option, item: CompletionItem, ) { + let original_completion_item = if fields_to_resolve == &CompletionFieldsToResolve::empty() { + None + } else { + Some(item.clone()) + }; let insert_replace_support = config.insert_replace_support().then_some(tdpp.position); let ref_match = item.ref_match(); @@ -393,16 +399,17 @@ fn completion_item( Vec::new() }; let (ref_resolve_data, resolve_data) = if something_to_resolve || !imports.is_empty() { - let mut item_index = acc.len(); let ref_resolve_data = if ref_match.is_some() { let ref_resolve_data = lsp_ext::CompletionResolveData { position: tdpp.clone(), imports: Vec::new(), version, trigger_character: completion_trigger_character, - completion_item_index: item_index, + for_ref: true, + completion_item_hash: original_completion_item + .as_ref() + .map(|item| completion_item_hash(item, true)), }; - item_index += 1; Some(to_value(ref_resolve_data).unwrap()) } else { None @@ -412,7 +419,10 @@ fn completion_item( imports, version, trigger_character: completion_trigger_character, - completion_item_index: item_index, + for_ref: false, + completion_item_hash: original_completion_item + .as_ref() + .map(|item| completion_item_hash(item, false)), }; (ref_resolve_data, Some(to_value(resolve_data).unwrap())) } else { diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index b7c536e027..0086382517 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@