From 256b446bdf806d214add5b51beb2ddefd638c4e2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Apr 2024 09:22:56 -0700 Subject: [PATCH] Refactor LSP adapter methods to compute labels in batches (#10097) Once we enable extensions to customize the labels of completions and symbols, this new structure will allow this to be done with a single WASM call, instead of one WASM call per completion / symbol. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers Co-authored-by: Marshall --- .../src/chat_panel/message_editor.rs | 10 +- crates/editor/src/editor.rs | 12 +- crates/language/src/buffer.rs | 53 +- crates/language/src/language.rs | 59 +- crates/language/src/proto.rs | 84 +-- crates/languages/src/python.rs | 20 +- crates/multi_buffer/src/multi_buffer.rs | 1 - crates/project/src/lsp_command.rs | 267 ++++----- crates/project/src/project.rs | 531 +++++++++++++----- 9 files changed, 589 insertions(+), 448 deletions(-) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index fbdded1cba..ac598a8c50 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -9,12 +9,12 @@ use gpui::{ Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace, }; use language::{ - language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion, - LanguageRegistry, LanguageServerId, ToOffset, + language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, + LanguageServerId, ToOffset, }; use lazy_static::lazy_static; use parking_lot::RwLock; -use project::search::SearchQuery; +use project::{search::SearchQuery, Completion}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; use theme::ThemeSettings; @@ -48,7 +48,7 @@ impl CompletionProvider for MessageEditorCompletionProvider { buffer: &Model, buffer_position: language::Anchor, cx: &mut ViewContext, - ) -> Task>> { + ) -> Task>> { let Some(handle) = self.0.upgrade() else { return Task::ready(Ok(Vec::new())); }; @@ -60,7 +60,7 @@ impl CompletionProvider for MessageEditorCompletionProvider { fn resolve_completions( &self, _completion_indices: Vec, - _completions: Arc>>, + _completions: Arc>>, _cx: &mut ViewContext, ) -> Task> { Task::ready(Ok(false)) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af8b41a592..0005a8fc02 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -74,12 +74,12 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion_provider::*; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; -use language::{char_kind, CharKind}; use language::{ + char_kind, language_settings::{self, all_language_settings, InlayHintSettings}, - markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction, - CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, - Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, + CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, + Point, Selection, SelectionGoal, TransactionId, }; use hover_links::{HoverLink, HoveredLinkState, InlayHighlight}; @@ -94,7 +94,9 @@ pub use multi_buffer::{ use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::project_settings::{GitGutterSetting, ProjectSettings}; -use project::{FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction}; +use project::{ + CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction, +}; use rand::prelude::*; use rpc::proto::*; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1220890958..eb67ae5151 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -13,7 +13,7 @@ use crate::{ SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint, }, - CodeLabel, LanguageScope, Outline, + LanguageScope, Outline, }; use anyhow::{anyhow, Context, Result}; pub use clock::ReplicaId; @@ -250,34 +250,6 @@ pub enum Documentation { MultiLineMarkdown(ParsedMarkdown), } -/// A completion provided by a language server -#[derive(Clone, Debug)] -pub struct Completion { - /// The range of the buffer that will be replaced. - pub old_range: Range, - /// The new text that will be inserted. - pub new_text: String, - /// A label for this completion that is shown in the menu. - pub label: CodeLabel, - /// The id of the language server that produced this completion. - pub server_id: LanguageServerId, - /// The documentation for this completion. - pub documentation: Option, - /// The raw completion provided by the language server. - pub lsp_completion: lsp::CompletionItem, -} - -/// A code action provided by a language server. -#[derive(Clone, Debug)] -pub struct CodeAction { - /// The id of the language server that produced this code action. - pub server_id: LanguageServerId, - /// The range of the buffer where this code action is applicable. - pub range: Range, - /// The raw code action provided by the language server. - pub lsp_action: lsp::CodeAction, -} - /// An operation used to synchronize this buffer with its other replicas. #[derive(Clone, Debug, PartialEq)] pub enum Operation { @@ -2526,6 +2498,11 @@ impl BufferSnapshot { .last() } + /// Returns the main [Language] + pub fn language(&self) -> Option<&Arc> { + self.language.as_ref() + } + /// Returns the [Language] at the given location. pub fn language_at(&self, position: D) -> Option<&Arc> { self.syntax_layer_at(position) @@ -3508,24 +3485,6 @@ impl IndentSize { } } -impl Completion { - /// A key that can be used to sort completions when displaying - /// them to the user. - pub fn sort_key(&self) -> (usize, &str) { - let kind_key = match self.lsp_completion.kind { - Some(lsp::CompletionItemKind::KEYWORD) => 0, - Some(lsp::CompletionItemKind::VARIABLE) => 1, - _ => 2, - }; - (kind_key, &self.label.text[self.label.filter_range.clone()]) - } - - /// Whether this completion is a snippet. - pub fn is_snippet(&self) -> bool { - self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) - } -} - #[cfg(any(test, feature = "test-support"))] pub struct TestFile { pub path: Arc, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 797589c5f6..ea5825e477 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -205,27 +205,26 @@ impl CachedLspAdapter { self.adapter.process_diagnostics(params) } - pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { - self.adapter.process_completion(completion_item).await + pub async fn process_completions(&self, completion_items: &mut [lsp::CompletionItem]) { + self.adapter.process_completions(completion_items).await } - pub async fn label_for_completion( + pub async fn labels_for_completions( &self, - completion_item: &lsp::CompletionItem, + completion_items: &[lsp::CompletionItem], language: &Arc, - ) -> Option { + ) -> Vec> { self.adapter - .label_for_completion(completion_item, language) + .labels_for_completions(completion_items, language) .await } - pub async fn label_for_symbol( + pub async fn labels_for_symbols( &self, - name: &str, - kind: lsp::SymbolKind, + symbols: &[(String, lsp::SymbolKind)], language: &Arc, - ) -> Option { - self.adapter.label_for_symbol(name, kind, language).await + ) -> Vec> { + self.adapter.labels_for_symbols(symbols, language).await } #[cfg(any(test, feature = "test-support"))] @@ -382,10 +381,24 @@ pub trait LspAdapter: 'static + Send + Sync { fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} - /// A callback called for each [`lsp::CompletionItem`] obtained from LSP server. - /// Some LspAdapter implementations might want to modify the obtained item to - /// change how it's displayed. - async fn process_completion(&self, _: &mut lsp::CompletionItem) {} + /// Post-processes completions provided by the language server. + async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {} + + async fn labels_for_completions( + &self, + completions: &[lsp::CompletionItem], + language: &Arc, + ) -> Vec> { + let mut labels = Vec::new(); + for (ix, completion) in completions.into_iter().enumerate() { + let label = self.label_for_completion(completion, language).await; + if let Some(label) = label { + labels.resize(ix + 1, None); + *labels.last_mut().unwrap() = Some(label); + } + } + labels + } async fn label_for_completion( &self, @@ -395,6 +408,22 @@ pub trait LspAdapter: 'static + Send + Sync { None } + async fn labels_for_symbols( + &self, + symbols: &[(String, lsp::SymbolKind)], + language: &Arc, + ) -> Vec> { + let mut labels = Vec::new(); + for (ix, (name, kind)) in symbols.into_iter().enumerate() { + let label = self.label_for_symbol(name, *kind, language).await; + if let Some(label) = label { + labels.resize(ix + 1, None); + *labels.last_mut().unwrap() = Some(label); + } + } + labels + } + async fn label_for_symbol( &self, _: &str, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index acfb3c9058..80c22a3b60 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,9 +1,6 @@ //! Handles conversions of `language` items to and from the [`rpc`] protocol. -use crate::{ - diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, - Language, LanguageRegistry, -}; +use crate::{diagnostic_set::DiagnosticEntry, CursorShape, Diagnostic}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use lsp::{DiagnosticSeverity, LanguageServerId}; @@ -466,85 +463,6 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option proto::Completion { - proto::Completion { - old_start: Some(serialize_anchor(&completion.old_range.start)), - old_end: Some(serialize_anchor(&completion.old_range.end)), - new_text: completion.new_text.clone(), - server_id: completion.server_id.0 as u64, - lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), - } -} - -/// Deserializes a [`Completion`] from the RPC representation. -pub async fn deserialize_completion( - completion: proto::Completion, - language: Option>, - language_registry: &Arc, -) -> Result { - let old_start = completion - .old_start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid old start"))?; - let old_end = completion - .old_end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid old end"))?; - let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?; - - let mut label = None; - if let Some(language) = language { - if let Some(adapter) = language_registry.lsp_adapters(&language).first() { - label = adapter - .label_for_completion(&lsp_completion, &language) - .await; - } - } - - Ok(Completion { - old_range: old_start..old_end, - new_text: completion.new_text, - label: label.unwrap_or_else(|| { - CodeLabel::plain( - lsp_completion.label.clone(), - lsp_completion.filter_text.as_deref(), - ) - }), - documentation: None, - server_id: LanguageServerId(completion.server_id as usize), - lsp_completion, - }) -} - -/// Serializes a [`CodeAction`] to be sent over RPC. -pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { - proto::CodeAction { - server_id: action.server_id.0 as u64, - start: Some(serialize_anchor(&action.range.start)), - end: Some(serialize_anchor(&action.range.end)), - lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(), - } -} - -/// Deserializes a [`CodeAction`] from the RPC representation. -pub fn deserialize_code_action(action: proto::CodeAction) -> Result { - let start = action - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid start"))?; - let end = action - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid end"))?; - let lsp_action = serde_json::from_slice(&action.lsp_action)?; - Ok(CodeAction { - server_id: LanguageServerId(action.server_id as usize), - range: start..end, - lsp_action, - }) -} - /// Serializes a [`Transaction`] to be sent over RPC. pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { proto::Transaction { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 7dad83a97d..fea4179627 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -83,7 +83,7 @@ impl LspAdapter for PythonLspAdapter { get_cached_server_binary(container_dir, &*self.node).await } - async fn process_completion(&self, item: &mut lsp::CompletionItem) { + async fn process_completions(&self, items: &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. @@ -94,14 +94,16 @@ impl LspAdapter for PythonLspAdapter { // 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.., ""); + for item in items { + let Some(sort_text) = &mut item.sort_text else { + continue; + }; + let mut parts = sort_text.split('.'); + let Some(first) = parts.next() else { continue }; + let Some(second) = parts.next() else { continue }; + let Some(_) = parts.next() else { continue }; + sort_text.replace_range(first.len() + second.len() + 1.., ""); + } } async fn label_for_completion( diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a6bc1251a0..0862f58b4c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7,7 +7,6 @@ use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; use git::diff::DiffHunk; use gpui::{AppContext, EventEmitter, Model, ModelContext}; -pub use language::Completion; use language::{ char_kind, language_settings::{language_settings, LanguageSettings}, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2b88d1cfa3..969a2d5759 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,7 +1,7 @@ use crate::{ - DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - MarkupContent, Project, ProjectTransaction, ResolveState, + CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, + InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, + LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -10,11 +10,10 @@ use futures::future; use gpui::{AppContext, AsyncAppContext, Model}; use language::{ language_settings::{language_settings, InlayHintKind}, - point_from_lsp, point_to_lsp, prepare_completion_documentation, + point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, - Unclipped, + OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; use lsp::{ CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId, @@ -1429,7 +1428,7 @@ impl LspCommand for GetHover { #[async_trait(?Send)] impl LspCommand for GetCompletions { - type Response = Vec; + type Response = Vec; type LspRequest = lsp::request::Completion; type ProtoRequest = proto::GetCompletions; @@ -1458,9 +1457,9 @@ impl LspCommand for GetCompletions { buffer: Model, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result> { + ) -> Result { let mut response_list = None; - let completions = if let Some(completions) = completions { + let mut completions = if let Some(completions) = completions { match completions { lsp::CompletionResponse::Array(completions) => completions, @@ -1480,147 +1479,120 @@ impl LspCommand for GetCompletions { })? .ok_or_else(|| anyhow!("no such language server"))?; - let completions = buffer.update(&mut cx, |buffer, cx| { - let language_registry = project.read(cx).languages().clone(); - let language = buffer.language().cloned(); + let mut completion_edits = Vec::new(); + buffer.update(&mut cx, |buffer, _cx| { let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); let mut range_for_token = None; - completions - .into_iter() - .filter_map(move |mut lsp_completion| { - 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); + completions.retain_mut(|lsp_completion| { + let edit = 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.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return false; + } + ( + 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 self.position != clipped_position { + log::info!("completion out of expected range"); + return false; + } + + let default_edit_range = response_list + .as_ref() + .and_then(|list| list.item_defaults.as_ref()) + .and_then(|defaults| defaults.edit_range.as_ref()) + .and_then(|range| match range { + CompletionListItemDefaultsEditRange::Range(r) => Some(r), + _ => None, + }); + + let range = if let Some(range) = default_edit_range { + let range = range_from_lsp(*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.0 || end != range.end.0 { 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 self.position != clipped_position { - log::info!("completion out of expected range"); - return None; + return false; } - let default_edit_range = response_list - .as_ref() - .and_then(|list| list.item_defaults.as_ref()) - .and_then(|defaults| defaults.edit_range.as_ref()) - .and_then(|range| match range { - CompletionListItemDefaultsEditRange::Range(r) => Some(r), - _ => None, - }); - - let range = if let Some(range) = default_edit_range { - let range = range_from_lsp(*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.0 || end != range.end.0 { - log::info!("completion out of expected range"); - return None; - } - - snapshot.anchor_before(start)..snapshot.anchor_after(end) - } else { - range_for_token - .get_or_insert_with(|| { - let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); - let range = if kind == Some(CharKind::Word) { - range - } else { - offset..offset - }; - - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .clone() - }; - - let text = lsp_completion - .insert_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - (range, text) - } - - Some(lsp::CompletionTextEdit::InsertAndReplace(edit)) => { - let range = range_from_lsp(edit.insert); - - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!("completion out of expected range"); - return None; - } - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - edit.new_text.clone(), - ) - } - }; - - let language_registry = language_registry.clone(); - let language = language.clone(); - let language_server_adapter = language_server_adapter.clone(); - LineEnding::normalize(&mut new_text); - Some(async move { - let mut label = None; - if let Some(language) = &language { - language_server_adapter - .process_completion(&mut lsp_completion) - .await; - label = language_server_adapter - .label_for_completion(&lsp_completion, language) - .await; - } - - let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { - Some( - prepare_completion_documentation( - lsp_docs, - &language_registry, - language.clone(), - ) - .await, - ) + snapshot.anchor_before(start)..snapshot.anchor_after(end) } else { - None + range_for_token + .get_or_insert_with(|| { + let offset = self.position.to_offset(&snapshot); + let (range, kind) = snapshot.surrounding_word(offset); + let range = if kind == Some(CharKind::Word) { + range + } else { + offset..offset + }; + + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .clone() }; - Completion { - old_range, - new_text, - label: label.unwrap_or_else(|| { - language::CodeLabel::plain( - lsp_completion.label.clone(), - lsp_completion.filter_text.as_deref(), - ) - }), - documentation, - server_id, - lsp_completion, + let text = lsp_completion + .insert_text + .as_ref() + .unwrap_or(&lsp_completion.label) + .clone(); + (range, text) + } + + Some(lsp::CompletionTextEdit::InsertAndReplace(edit)) => { + let range = range_from_lsp(edit.insert); + + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return false; } - }) - }) + ( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + edit.new_text.clone(), + ) + } + }; + + completion_edits.push(edit); + true + }); })?; - Ok(future::join_all(completions).await) + language_server_adapter + .process_completions(&mut completions) + .await; + + Ok(completions + .into_iter() + .zip(completion_edits) + .map(|(lsp_completion, (old_range, mut new_text))| { + LineEnding::normalize(&mut new_text); + CoreCompletion { + old_range, + new_text, + server_id, + lsp_completion, + } + }) + .collect()) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { @@ -1656,7 +1628,7 @@ impl LspCommand for GetCompletions { } fn response_to_proto( - completions: Vec, + completions: Vec, _: &mut Project, _: PeerId, buffer_version: &clock::Global, @@ -1665,7 +1637,7 @@ impl LspCommand for GetCompletions { proto::GetCompletionsResponse { completions: completions .iter() - .map(language::proto::serialize_completion) + .map(Project::serialize_completion) .collect(), version: serialize_version(buffer_version), } @@ -1674,26 +1646,21 @@ impl LspCommand for GetCompletions { async fn response_from_proto( self, message: proto::GetCompletionsResponse, - project: Model, + _project: Model, buffer: Model, mut cx: AsyncAppContext, - ) -> Result> { + ) -> Result { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) })? .await?; - let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?; - let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?; - let completions = message.completions.into_iter().map(|completion| { - language::proto::deserialize_completion( - completion, - language.clone(), - &language_registry, - ) - }); - future::try_join_all(completions).await + message + .completions + .into_iter() + .map(Project::deserialize_completion) + .collect() } fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result { @@ -1816,7 +1783,7 @@ impl LspCommand for GetCodeActions { proto::GetCodeActionsResponse { actions: code_actions .iter() - .map(language::proto::serialize_code_action) + .map(Project::serialize_code_action) .collect(), version: serialize_version(buffer_version), } @@ -1837,7 +1804,7 @@ impl LspCommand for GetCodeActions { message .actions .into_iter() - .map(language::proto::deserialize_code_action) + .map(Project::deserialize_code_action) .collect() } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9dbc3f122..b6436257cd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -40,16 +40,16 @@ use gpui::{ use itertools::Itertools; use language::{ language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, - markdown, point_to_lsp, + markdown, point_to_lsp, prepare_completion_documentation, proto::{ deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeAction, - CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, - Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, - LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, - ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel, + Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, Event as BufferEvent, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, + Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, + ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -81,7 +81,7 @@ use std::{ env, ffi::OsStr, hash::Hash, - io, mem, + io, iter, mem, num::NonZeroU32, ops::Range, path::{self, Component, Path, PathBuf}, @@ -379,6 +379,43 @@ pub struct InlayHint { pub resolve_state: ResolveState, } +/// A completion provided by a language server +#[derive(Clone, Debug)] +pub struct Completion { + /// The range of the buffer that will be replaced. + pub old_range: Range, + /// The new text that will be inserted. + pub new_text: String, + /// A label for this completion that is shown in the menu. + pub label: CodeLabel, + /// The id of the language server that produced this completion. + pub server_id: LanguageServerId, + /// The documentation for this completion. + pub documentation: Option, + /// The raw completion provided by the language server. + pub lsp_completion: lsp::CompletionItem, +} + +/// A completion provided by a language server +#[derive(Clone, Debug)] +struct CoreCompletion { + old_range: Range, + new_text: String, + server_id: LanguageServerId, + lsp_completion: lsp::CompletionItem, +} + +/// A code action provided by a language server. +#[derive(Clone, Debug)] +pub struct CodeAction { + /// The id of the language server that produced this code action. + pub server_id: LanguageServerId, + /// The range of the buffer where this code action is applicable. + pub range: Range, + /// The raw code action provided by the language server. + pub lsp_action: lsp::CodeAction, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolveState { Resolved, @@ -450,6 +487,17 @@ pub struct Symbol { pub signature: [u8; 32], } +#[derive(Clone, Debug)] +struct CoreSymbol { + pub language_server_name: LanguageServerName, + pub source_worktree_id: WorktreeId, + pub path: ProjectPath, + pub name: String, + pub kind: lsp::SymbolKind, + pub range: Range>, + pub signature: [u8; 32], +} + #[derive(Clone, Debug, PartialEq)] pub struct HoverBlock { pub text: String, @@ -4931,6 +4979,8 @@ impl Project { } pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { + let language_registry = self.languages.clone(); + if self.is_local() { let mut requests = Vec::new(); for ((worktree_id, _), server_id) in self.language_server_ids.iter() { @@ -5005,18 +5055,14 @@ impl Project { None => return Ok(Vec::new()), }; - let symbols = this.update(&mut cx, |this, cx| { - let mut symbols = Vec::new(); - for ( - adapter, - adapter_language, - source_worktree, - worktree_abs_path, - lsp_symbols, - ) in responses - { - symbols.extend(lsp_symbols.into_iter().filter_map( - |(symbol_name, symbol_kind, symbol_location)| { + let mut symbols = Vec::new(); + for (adapter, adapter_language, source_worktree, worktree_abs_path, lsp_symbols) in + responses + { + let core_symbols = this.update(&mut cx, |this, cx| { + lsp_symbols + .into_iter() + .filter_map(|(symbol_name, symbol_kind, symbol_location)| { let abs_path = symbol_location.uri.to_file_path().ok()?; let source_worktree = source_worktree.upgrade()?; let source_worktree_id = source_worktree.read(cx).id(); @@ -5039,62 +5085,52 @@ impl Project { path: path.into(), }; let signature = this.symbol_signature(&project_path); - let adapter_language = adapter_language.clone(); - let language = this - .languages - .language_for_file_path(&project_path.path) - .unwrap_or_else(move |_| adapter_language); - let adapter = adapter.clone(); - Some(async move { - let language = language.await; - let label = adapter - .label_for_symbol(&symbol_name, symbol_kind, &language) - .await; - - Symbol { - language_server_name: adapter.name.clone(), - source_worktree_id, - path: project_path, - label: label.unwrap_or_else(|| { - CodeLabel::plain(symbol_name.clone(), None) - }), - kind: symbol_kind, - name: symbol_name, - range: range_from_lsp(symbol_location.range), - signature, - } + Some(CoreSymbol { + language_server_name: adapter.name.clone(), + source_worktree_id, + path: project_path, + kind: symbol_kind, + name: symbol_name, + range: range_from_lsp(symbol_location.range), + signature, }) - }, - )); - } + }) + .collect() + })?; - symbols - })?; + populate_labels_for_symbols( + core_symbols, + &language_registry, + Some(adapter_language), + Some(adapter), + &mut symbols, + ) + .await; + } - Ok(futures::future::join_all(symbols).await) + Ok(symbols) }) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::GetProjectSymbols { project_id, query: query.to_string(), }); - cx.spawn(move |this, mut cx| async move { + cx.foreground_executor().spawn(async move { let response = request.await?; let mut symbols = Vec::new(); - if let Some(this) = this.upgrade() { - let new_symbols = this.update(&mut cx, |this, _| { - response - .symbols - .into_iter() - .map(|symbol| this.deserialize_symbol(symbol)) - .collect::>() - })?; - symbols = futures::future::join_all(new_symbols) - .await - .into_iter() - .filter_map(|symbol| symbol.log_err()) - .collect::>(); - } + let core_symbols = response + .symbols + .into_iter() + .filter_map(|symbol| Self::deserialize_symbol(symbol).log_err()) + .collect::>(); + populate_labels_for_symbols( + core_symbols, + &language_registry, + None, + None, + &mut symbols, + ) + .await; Ok(symbols) }) } else { @@ -5262,10 +5298,13 @@ impl Project { position: PointUtf16, cx: &mut ModelContext, ) -> Task>> { + let language_registry = self.languages.clone(); + if self.is_local() { let snapshot = buffer.read(cx).snapshot(); let offset = position.to_offset(&snapshot); let scope = snapshot.language_scope_at(offset); + let language = snapshot.language().cloned(); let server_ids: Vec<_> = self .language_servers_for_buffer(buffer.read(cx), cx) @@ -5284,30 +5323,69 @@ impl Project { let mut tasks = Vec::with_capacity(server_ids.len()); this.update(&mut cx, |this, cx| { for server_id in server_ids { - tasks.push(this.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetCompletions { position }, - cx, + let lsp_adapter = this.language_server_adapter_for_id(server_id); + tasks.push(( + lsp_adapter, + this.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + GetCompletions { position }, + cx, + ), )); } })?; let mut completions = Vec::new(); - for task in tasks { + for (lsp_adapter, task) in tasks { if let Ok(new_completions) = task.await { - completions.extend_from_slice(&new_completions); + populate_labels_for_completions( + new_completions, + &language_registry, + language.clone(), + lsp_adapter, + &mut completions, + ) + .await; } } Ok(completions) }) } else if let Some(project_id) = self.remote_id() { - self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx) + let task = self.send_lsp_proto_request( + buffer.clone(), + project_id, + GetCompletions { position }, + cx, + ); + let language = buffer.read(cx).language().cloned(); + + // In the future, we should provide project guests with the names of LSP adapters, + // so that they can use the correct LSP adapter when computing labels. For now, + // guests just use the first LSP adapter associated with the buffer's language. + let lsp_adapter = language + .as_ref() + .and_then(|language| language_registry.lsp_adapters(language).first().cloned()); + + cx.foreground_executor().spawn(async move { + let completions = task.await?; + let mut result = Vec::new(); + populate_labels_for_completions( + completions, + &language_registry, + language, + lsp_adapter, + &mut result, + ) + .await; + Ok(result) + }) } else { Task::ready(Ok(Default::default())) } } + pub fn completions( &self, buffer: &Model, @@ -5573,7 +5651,12 @@ impl Project { .request(proto::ApplyCompletionAdditionalEdits { project_id, buffer_id: buffer_id.into(), - completion: Some(language::proto::serialize_completion(&completion)), + completion: Some(Self::serialize_completion(&CoreCompletion { + old_range: completion.old_range, + new_text: completion.new_text, + server_id: completion.server_id, + lsp_completion: completion.lsp_completion, + })), }) .await?; @@ -5757,7 +5840,7 @@ impl Project { let request = proto::ApplyCodeAction { project_id, buffer_id: buffer_handle.read(cx).remote_id().into(), - action: Some(language::proto::serialize_code_action(&action)), + action: Some(Self::serialize_code_action(&action)), }; cx.spawn(move |this, mut cx| async move { let response = client @@ -8548,30 +8631,40 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let languages = this.update(&mut cx, |this, _| this.languages.clone())?; - let (buffer, completion) = this.update(&mut cx, |this, cx| { + let (buffer, completion) = this.update(&mut cx, |this, _| { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; let buffer = this .opened_buffers .get(&buffer_id) .and_then(|buffer| buffer.upgrade()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; - let language = buffer.read(cx).language(); - let completion = language::proto::deserialize_completion( + let completion = Self::deserialize_completion( envelope .payload .completion .ok_or_else(|| anyhow!("invalid completion"))?, - language.cloned(), - &languages, - ); - Ok::<_, anyhow::Error>((buffer, completion)) + )?; + anyhow::Ok((buffer, completion)) })??; - let completion = completion.await?; - let apply_additional_edits = this.update(&mut cx, |this, cx| { - this.apply_additional_edits_for_completion(buffer, completion, false, cx) + this.apply_additional_edits_for_completion( + buffer, + Completion { + old_range: completion.old_range, + new_text: completion.new_text, + lsp_completion: completion.lsp_completion, + server_id: completion.server_id, + documentation: None, + label: CodeLabel { + text: Default::default(), + runs: Default::default(), + filter_range: Default::default(), + }, + }, + false, + cx, + ) })?; Ok(proto::ApplyCompletionAdditionalEditsResponse { @@ -8623,7 +8716,7 @@ impl Project { mut cx: AsyncAppContext, ) -> Result { let sender_id = envelope.original_sender_id()?; - let action = language::proto::deserialize_code_action( + let action = Self::deserialize_code_action( envelope .payload .action @@ -8984,9 +9077,7 @@ impl Project { .payload .symbol .ok_or_else(|| anyhow!("invalid symbol"))?; - let symbol = this - .update(&mut cx, |this, _cx| this.deserialize_symbol(symbol))? - .await?; + let symbol = Self::deserialize_symbol(symbol)?; let symbol = this.update(&mut cx, |this, _| { let signature = this.symbol_signature(&symbol.path); if signature == symbol.signature { @@ -8996,7 +9087,25 @@ impl Project { } })??; let buffer = this - .update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))? + .update(&mut cx, |this, cx| { + this.open_buffer_for_symbol( + &Symbol { + language_server_name: symbol.language_server_name, + source_worktree_id: symbol.source_worktree_id, + path: symbol.path, + name: symbol.name, + kind: symbol.kind, + range: symbol.range, + signature: symbol.signature, + label: CodeLabel { + text: Default::default(), + runs: Default::default(), + filter_range: Default::default(), + }, + }, + cx, + ) + })? .await?; this.update(&mut cx, |this, cx| { @@ -9350,11 +9459,7 @@ impl Project { Ok(()) } - fn deserialize_symbol( - &self, - serialized_symbol: proto::Symbol, - ) -> impl Future> { - let languages = self.languages.clone(); + fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result { let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); let kind = unsafe { mem::transmute(serialized_symbol.kind) }; @@ -9362,49 +9467,83 @@ impl Project { worktree_id, path: PathBuf::from(serialized_symbol.path).into(), }; - let language = languages.language_for_file_path(&path.path); - async move { - let language = language.await.log_err(); - let adapter = language - .as_ref() - .and_then(|language| languages.lsp_adapters(language).first().cloned()); - let start = serialized_symbol - .start - .ok_or_else(|| anyhow!("invalid start"))?; - let end = serialized_symbol - .end - .ok_or_else(|| anyhow!("invalid end"))?; - Ok(Symbol { - language_server_name: LanguageServerName( - serialized_symbol.language_server_name.into(), - ), - source_worktree_id, - path, - label: { - match language.as_ref().zip(adapter.as_ref()) { - Some((language, adapter)) => { - adapter - .label_for_symbol(&serialized_symbol.name, kind, language) - .await - } - None => None, - } - .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None)) - }, + let start = serialized_symbol + .start + .ok_or_else(|| anyhow!("invalid start"))?; + let end = serialized_symbol + .end + .ok_or_else(|| anyhow!("invalid end"))?; + Ok(CoreSymbol { + language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), + source_worktree_id, + path, + name: serialized_symbol.name, + range: Unclipped(PointUtf16::new(start.row, start.column)) + ..Unclipped(PointUtf16::new(end.row, end.column)), + kind, + signature: serialized_symbol + .signature + .try_into() + .map_err(|_| anyhow!("invalid signature"))?, + }) + } - name: serialized_symbol.name, - range: Unclipped(PointUtf16::new(start.row, start.column)) - ..Unclipped(PointUtf16::new(end.row, end.column)), - kind, - signature: serialized_symbol - .signature - .try_into() - .map_err(|_| anyhow!("invalid signature"))?, - }) + fn serialize_completion(completion: &CoreCompletion) -> proto::Completion { + proto::Completion { + old_start: Some(serialize_anchor(&completion.old_range.start)), + old_end: Some(serialize_anchor(&completion.old_range.end)), + new_text: completion.new_text.clone(), + server_id: completion.server_id.0 as u64, + lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), } } + fn deserialize_completion(completion: proto::Completion) -> Result { + let old_start = completion + .old_start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old start"))?; + let old_end = completion + .old_end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old end"))?; + let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?; + + Ok(CoreCompletion { + old_range: old_start..old_end, + new_text: completion.new_text, + server_id: LanguageServerId(completion.server_id as usize), + lsp_completion, + }) + } + + fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { + proto::CodeAction { + server_id: action.server_id.0 as u64, + start: Some(serialize_anchor(&action.range.start)), + end: Some(serialize_anchor(&action.range.end)), + lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(), + } + } + + fn deserialize_code_action(action: proto::CodeAction) -> Result { + let start = action + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid start"))?; + let end = action + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid end"))?; + let lsp_action = serde_json::from_slice(&action.lsp_action)?; + Ok(CodeAction { + server_id: LanguageServerId(action.server_id as usize), + range: start..end, + lsp_action, + }) + } + async fn handle_buffer_saved( this: Model, envelope: TypedEnvelope, @@ -9697,6 +9836,114 @@ impl Project { } } +async fn populate_labels_for_symbols( + symbols: Vec, + language_registry: &Arc, + default_language: Option>, + lsp_adapter: Option>, + output: &mut Vec, +) { + let mut symbols_by_language = HashMap::>, Vec>::default(); + + for symbol in symbols { + let language = language_registry + .language_for_file_path(&symbol.path.path) + .await + .log_err() + .or_else(|| default_language.clone()); + symbols_by_language + .entry(language) + .or_default() + .push(symbol); + } + + let mut label_params = Vec::new(); + for (language, mut symbols) in symbols_by_language { + label_params.clear(); + label_params.extend( + symbols + .iter_mut() + .map(|symbol| (mem::take(&mut symbol.name), symbol.kind)), + ); + + let mut labels = Vec::new(); + if let Some(language) = language { + let lsp_adapter = lsp_adapter + .clone() + .or_else(|| language_registry.lsp_adapters(&language).first().cloned()); + if let Some(lsp_adapter) = lsp_adapter { + labels = lsp_adapter + .labels_for_symbols(&label_params, &language) + .await; + } + } + + for ((symbol, (name, _)), label) in symbols + .into_iter() + .zip(label_params.drain(..)) + .zip(labels.into_iter().chain(iter::repeat(None))) + { + output.push(Symbol { + language_server_name: symbol.language_server_name, + source_worktree_id: symbol.source_worktree_id, + path: symbol.path, + label: label.unwrap_or_else(|| CodeLabel::plain(name.clone(), None)), + name, + kind: symbol.kind, + range: symbol.range, + signature: symbol.signature, + }); + } + } +} + +async fn populate_labels_for_completions( + mut new_completions: Vec, + language_registry: &Arc, + language: Option>, + lsp_adapter: Option>, + completions: &mut Vec, +) { + let lsp_completions = new_completions + .iter_mut() + .map(|completion| mem::take(&mut completion.lsp_completion)) + .collect::>(); + + let labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) { + lsp_adapter + .labels_for_completions(&lsp_completions, language) + .await + } else { + Vec::new() + }; + + for ((completion, lsp_completion), label) in new_completions + .into_iter() + .zip(lsp_completions) + .zip(labels.into_iter().chain(iter::repeat(None))) + { + let documentation = if let Some(docs) = &lsp_completion.documentation { + Some(prepare_completion_documentation(docs, &language_registry, language.clone()).await) + } else { + None + }; + + completions.push(Completion { + old_range: completion.old_range, + new_text: completion.new_text, + label: label.unwrap_or_else(|| { + CodeLabel::plain( + lsp_completion.label.clone(), + lsp_completion.filter_text.as_deref(), + ) + }), + server_id: completion.server_id, + documentation, + lsp_completion, + }) + } +} + fn deserialize_code_actions(code_actions: &HashMap) -> Vec { code_actions .iter() @@ -10190,6 +10437,24 @@ impl Item for Buffer { } } +impl Completion { + /// A key that can be used to sort completions when displaying + /// them to the user. + pub fn sort_key(&self) -> (usize, &str) { + let kind_key = match self.lsp_completion.kind { + Some(lsp::CompletionItemKind::KEYWORD) => 0, + Some(lsp::CompletionItemKind::VARIABLE) => 1, + _ => 2, + }; + (kind_key, &self.label.text[self.label.filter_range.clone()]) + } + + /// Whether this completion is a snippet. + pub fn is_snippet(&self) -> bool { + self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) + } +} + async fn wait_for_loading_buffer( mut receiver: postage::watch::Receiver, Arc>>>, ) -> Result, Arc> {