diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 5e9319b128..47d724866d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -135,6 +135,10 @@ impl CachedLspAdapter { self.adapter.process_diagnostics(params).await } + pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { + self.adapter.process_completion(completion_item).await + } + pub async fn label_for_completion( &self, completion_item: &lsp::CompletionItem, @@ -175,6 +179,8 @@ pub trait LspAdapter: 'static + Send + Sync { async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + async fn process_completion(&self, _: &mut lsp::CompletionItem) {} + async fn label_for_completion( &self, _: &lsp::CompletionItem, @@ -826,6 +832,12 @@ impl Language { } } + pub async fn process_completion(self: &Arc, completion: &mut lsp::CompletionItem) { + if let Some(adapter) = self.adapter.as_ref() { + adapter.process_completion(completion).await; + } + } + pub async fn label_for_completion( self: &Arc, completion: &lsp::CompletionItem, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index f93d99f76b..674ce4f50e 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -426,10 +426,11 @@ pub async fn deserialize_completion( .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("invalid old end"))?; let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?; - let label = match language { - Some(l) => l.label_for_completion(&lsp_completion).await, - None => None, - }; + + let mut label = None; + if let Some(language) = language { + label = language.label_for_completion(&lsp_completion).await; + } Ok(Completion { old_range: old_start..old_end, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e1a0b9c00a..a7124a1969 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3329,88 +3329,91 @@ impl Project { let snapshot = this.snapshot(); let clipped_position = this.clip_point_utf16(position, Bias::Left); let mut range_for_token = None; - completions.into_iter().filter_map(move |lsp_completion| { - // For now, we can only handle additional edits if they are returned - // when resolving the completion, not if they are present initially. - if lsp_completion - .additional_text_edits - .as_ref() - .map_or(false, |edits| !edits.is_empty()) - { - return None; - } - - let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { - // If the language server provides a range to overwrite, then - // check that the range is valid. - Some(lsp::CompletionTextEdit::Edit(edit)) => { - let range = range_from_lsp(edit.range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start || end != range.end { - log::info!("completion out of expected range"); - return None; - } - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - edit.new_text.clone(), - ) - } - // If the language server does not provide a range, then infer - // the range based on the syntax tree. - None => { - if position != clipped_position { - log::info!("completion out of expected range"); - return None; - } - let Range { start, end } = range_for_token - .get_or_insert_with(|| { - let offset = position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); - if kind == Some(CharKind::Word) { - range - } else { - offset..offset - } - }) - .clone(); - let text = lsp_completion - .insert_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - text, - ) - } - Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { - log::info!("unsupported insert/replace completion"); + completions + .into_iter() + .filter_map(move |mut lsp_completion| { + // For now, we can only handle additional edits if they are returned + // when resolving the completion, not if they are present initially. + if lsp_completion + .additional_text_edits + .as_ref() + .map_or(false, |edits| !edits.is_empty()) + { return None; } - }; - LineEnding::normalize(&mut new_text); - let language = language.clone(); - Some(async move { - let label = if let Some(language) = language { - language.label_for_completion(&lsp_completion).await - } else { - None - }; - Completion { - old_range, - new_text, - label: label.unwrap_or_else(|| { - CodeLabel::plain( - lsp_completion.label.clone(), - lsp_completion.filter_text.as_deref(), + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() + { + // If the language server provides a range to overwrite, then + // check that the range is valid. + Some(lsp::CompletionTextEdit::Edit(edit)) => { + let range = range_from_lsp(edit.range); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start || end != range.end { + log::info!("completion out of expected range"); + return None; + } + ( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + edit.new_text.clone(), ) - }), - lsp_completion, - } + } + // If the language server does not provide a range, then infer + // the range based on the syntax tree. + None => { + if position != clipped_position { + log::info!("completion out of expected range"); + return None; + } + let Range { start, end } = range_for_token + .get_or_insert_with(|| { + let offset = position.to_offset(&snapshot); + let (range, kind) = snapshot.surrounding_word(offset); + if kind == Some(CharKind::Word) { + range + } else { + offset..offset + } + }) + .clone(); + let text = lsp_completion + .insert_text + .as_ref() + .unwrap_or(&lsp_completion.label) + .clone(); + ( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + text, + ) + } + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { + log::info!("unsupported insert/replace completion"); + return None; + } + }; + + LineEnding::normalize(&mut new_text); + let language = language.clone(); + Some(async move { + let mut label = None; + if let Some(language) = language { + language.process_completion(&mut lsp_completion).await; + label = language.label_for_completion(&lsp_completion).await; + } + Completion { + old_range, + new_text, + label: label.unwrap_or_else(|| { + CodeLabel::plain( + lsp_completion.label.clone(), + lsp_completion.filter_text.as_deref(), + ) + }), + lsp_completion, + } + }) }) - }) }); Ok(futures::future::join_all(completions).await) diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index e6e55eeac4..ba6ccf7bf0 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -87,6 +87,25 @@ impl LspAdapter for PythonLspAdapter { .log_err() } + async fn process_completion(&self, item: &mut lsp::CompletionItem) { + // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. + // Where `XX` is the sorting category, `YYYY` is based on most recent usage, + // and `name` is the symbol name itself. + // + // Because the the symbol name is included, there generally are not ties when + // sorting by the `sortText`, so the symbol's fuzzy match score is not taken + // into account. Here, we remove the symbol name from the sortText in order + // to allow our own fuzzy score to be used to break ties. + // + // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 + let Some(sort_text) = &mut item.sort_text else { return }; + let mut parts = sort_text.split('.'); + let Some(first) = parts.next() else { return }; + let Some(second) = parts.next() else { return }; + let Some(_) = parts.next() else { return }; + sort_text.replace_range(first.len() + second.len() + 1.., ""); + } + async fn label_for_completion( &self, item: &lsp::CompletionItem,