Draft completion hashing

This commit is contained in:
Kirill Bulatov 2024-12-09 19:53:30 +02:00
parent 99a6ecd41e
commit 62d97d9ba7
9 changed files with 142 additions and 24 deletions

8
Cargo.lock generated
View file

@ -1659,6 +1659,7 @@ dependencies = [
"hir-def", "hir-def",
"hir-ty", "hir-ty",
"ide", "ide",
"ide-completion",
"ide-db", "ide-db",
"ide-ssr", "ide-ssr",
"intern", "intern",
@ -1687,6 +1688,7 @@ dependencies = [
"stdx", "stdx",
"syntax", "syntax",
"syntax-bridge", "syntax-bridge",
"tenthash",
"test-fixture", "test-fixture",
"test-utils", "test-utils",
"tikv-jemallocator", "tikv-jemallocator",
@ -1990,6 +1992,12 @@ dependencies = [
"tt", "tt",
] ]
[[package]]
name = "tenthash"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d67f9f3cf70e0852941d7bc3cb884b49b24b8ee956baf91ad0abae31f5ef11fb"
[[package]] [[package]]
name = "test-fixture" name = "test-fixture"
version = "0.0.0" version = "0.0.0"

View file

@ -346,8 +346,7 @@ pub enum CompletionItemKind {
impl_from!(SymbolKind for CompletionItemKind); impl_from!(SymbolKind for CompletionItemKind);
impl CompletionItemKind { impl CompletionItemKind {
#[cfg(test)] pub fn tag(self) -> &'static str {
pub(crate) fn tag(self) -> &'static str {
match self { match self {
CompletionItemKind::SymbolKind(kind) => match kind { CompletionItemKind::SymbolKind(kind) => match kind {
SymbolKind::Attribute => "at", SymbolKind::Attribute => "at",

View file

@ -34,6 +34,7 @@ pub use crate::{
config::{CallableSnippets, CompletionConfig}, config::{CallableSnippets, CompletionConfig},
item::{ item::{
CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevancePostfixMatch, CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevancePostfixMatch,
CompletionRelevanceReturnType, CompletionRelevanceTypeMatch,
}, },
snippet::{Snippet, SnippetScope}, snippet::{Snippet, SnippetScope},
}; };

View file

@ -24,6 +24,7 @@ anyhow.workspace = true
crossbeam-channel.workspace = true crossbeam-channel.workspace = true
dirs = "5.0.1" dirs = "5.0.1"
dissimilar.workspace = true dissimilar.workspace = true
ide-completion.workspace = true
itertools.workspace = true itertools.workspace = true
scip = "0.5.1" scip = "0.5.1"
lsp-types = { version = "=0.95.0", features = ["proposed"] } lsp-types = { version = "=0.95.0", features = ["proposed"] }
@ -34,6 +35,7 @@ rayon.workspace = true
rustc-hash.workspace = true rustc-hash.workspace = true
serde_json = { workspace = true, features = ["preserve_order"] } serde_json = { workspace = true, features = ["preserve_order"] }
serde.workspace = true serde.workspace = true
tenthash = "0.4.0"
num_cpus = "1.15.0" num_cpus = "1.15.0"
mimalloc = { version = "0.1.30", default-features = false, optional = true } mimalloc = { version = "0.1.30", default-features = false, optional = true }
lsp-server.workspace = true lsp-server.workspace = true
@ -90,13 +92,13 @@ jemalloc = ["jemallocator", "profile/jemalloc"]
force-always-assert = ["always-assert/force"] force-always-assert = ["always-assert/force"]
sysroot-abi = [] sysroot-abi = []
in-rust-tree = [ in-rust-tree = [
"sysroot-abi", "sysroot-abi",
"syntax/in-rust-tree", "syntax/in-rust-tree",
"parser/in-rust-tree", "parser/in-rust-tree",
"hir/in-rust-tree", "hir/in-rust-tree",
"hir-def/in-rust-tree", "hir-def/in-rust-tree",
"hir-ty/in-rust-tree", "hir-ty/in-rust-tree",
"load-cargo/in-rust-tree", "load-cargo/in-rust-tree",
] ]
[lints] [lints]

View file

@ -36,6 +36,7 @@ use triomphe::Arc;
use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
use crate::{ use crate::{
completion_item_hash,
config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
diagnostics::convert_diagnostic, diagnostics::convert_diagnostic,
global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot}, global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot},
@ -1122,12 +1123,15 @@ pub(crate) fn handle_completion_resolve(
return Ok(original_completion); return Ok(original_completion);
}; };
let source_root = snap.analysis.source_root_id(file_id)?; 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)); let mut forced_resolve_completions_config = snap.config.completion(Some(source_root));
forced_resolve_completions_config.fields_to_resolve = CompletionFieldsToResolve::empty(); forced_resolve_completions_config.fields_to_resolve = CompletionFieldsToResolve::empty();
let position = FilePosition { file_id, offset }; let position = FilePosition { file_id, offset };
let Some(resolved_completions) = snap.analysis.completions( let Some(completions) = snap.analysis.completions(
&forced_resolve_completions_config, &forced_resolve_completions_config,
position, position,
resolve_data.trigger_character, resolve_data.trigger_character,
@ -1135,6 +1139,14 @@ pub(crate) fn handle_completion_resolve(
else { else {
return Ok(original_completion); 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( let mut resolved_completions = to_proto::completion_items(
&snap.config, &snap.config,
&forced_resolve_completions_config.fields_to_resolve, &forced_resolve_completions_config.fields_to_resolve,
@ -1142,15 +1154,11 @@ pub(crate) fn handle_completion_resolve(
snap.file_version(position.file_id), snap.file_version(position.file_id),
resolve_data.position, resolve_data.position,
resolve_data.trigger_character, resolve_data.trigger_character,
resolved_completions, vec![corresponding_completion],
); );
let Some(mut resolved_completion) = resolved_completions.pop() else {
let mut resolved_completion = return Ok(original_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);
};
if !resolve_data.imports.is_empty() { if !resolve_data.imports.is_empty() {
let additional_edits = snap let additional_edits = snap

View file

@ -47,7 +47,9 @@ use self::lsp::ext as lsp_ext;
#[cfg(test)] #[cfg(test)]
mod integrated_benchmarks; mod integrated_benchmarks;
use ide::{CompletionItem, CompletionRelevance, TextEdit, TextRange};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tenthash::TentHasher;
pub use crate::{ pub use crate::{
lsp::capabilities::server_capabilities, main_loop::main_loop, reload::ws_to_crate_graph, lsp::capabilities::server_capabilities, main_loop::main_loop, reload::ws_to_crate_graph,
@ -61,3 +63,90 @@ pub fn from_json<T: DeserializeOwned>(
serde_json::from_value(json.clone()) serde_json::from_value(json.clone())
.map_err(|e| anyhow::format_err!("Failed to deserialize {what}: {e}; {json}")) .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()
}

View file

@ -826,7 +826,8 @@ pub struct CompletionResolveData {
pub imports: Vec<CompletionImport>, pub imports: Vec<CompletionImport>,
pub version: Option<i32>, pub version: Option<i32>,
pub trigger_character: Option<char>, pub trigger_character: Option<char>,
pub completion_item_index: usize, pub for_ref: bool,
pub completion_item_hash: Option<[u8; 20]>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -21,6 +21,7 @@ use serde_json::to_value;
use vfs::AbsPath; use vfs::AbsPath;
use crate::{ use crate::{
completion_item_hash,
config::{CallInfoConfig, Config}, config::{CallInfoConfig, Config},
global_state::GlobalStateSnapshot, global_state::GlobalStateSnapshot,
line_index::{LineEndings, LineIndex, PositionEncoding}, line_index::{LineEndings, LineIndex, PositionEncoding},
@ -274,6 +275,11 @@ fn completion_item(
completion_trigger_character: Option<char>, completion_trigger_character: Option<char>,
item: CompletionItem, 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 insert_replace_support = config.insert_replace_support().then_some(tdpp.position);
let ref_match = item.ref_match(); let ref_match = item.ref_match();
@ -393,16 +399,17 @@ fn completion_item(
Vec::new() Vec::new()
}; };
let (ref_resolve_data, resolve_data) = if something_to_resolve || !imports.is_empty() { 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 = if ref_match.is_some() {
let ref_resolve_data = lsp_ext::CompletionResolveData { let ref_resolve_data = lsp_ext::CompletionResolveData {
position: tdpp.clone(), position: tdpp.clone(),
imports: Vec::new(), imports: Vec::new(),
version, version,
trigger_character: completion_trigger_character, 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()) Some(to_value(ref_resolve_data).unwrap())
} else { } else {
None None
@ -412,7 +419,10 @@ fn completion_item(
imports, imports,
version, version,
trigger_character: completion_trigger_character, 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())) (ref_resolve_data, Some(to_value(resolve_data).unwrap()))
} else { } else {

View file

@ -1,5 +1,5 @@
<!--- <!---
lsp/ext.rs hash: 96f88b7a5d0080c6 lsp/ext.rs hash: 7d77940d7e63a8b
If you need to change the above hash to make the test pass, please check if you If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: need to adjust this doc as well and ping this issue: