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 <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-04-03 09:22:56 -07:00 committed by GitHub
parent ef3d04efe6
commit 256b446bdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 589 additions and 448 deletions

View File

@ -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>,
buffer_position: language::Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<Vec<language::Completion>>> {
) -> Task<anyhow::Result<Vec<Completion>>> {
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<usize>,
_completions: Arc<RwLock<Box<[language::Completion]>>>,
_completions: Arc<RwLock<Box<[Completion]>>>,
_cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false))

View File

@ -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};

View File

@ -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<Anchor>,
/// 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<Documentation>,
/// 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<Anchor>,
/// 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<Language>> {
self.language.as_ref()
}
/// Returns the [Language] at the given location.
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
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<Path>,

View File

@ -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<Language>,
) -> Option<CodeLabel> {
) -> Vec<Option<CodeLabel>> {
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<Language>,
) -> Option<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await
) -> Vec<Option<CodeLabel>> {
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<Language>,
) -> Vec<Option<CodeLabel>> {
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<Language>,
) -> Vec<Option<CodeLabel>> {
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,

View File

@ -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<c
})
}
/// Serializes a [`Completion`] to be sent over RPC.
pub fn serialize_completion(completion: &Completion) -> 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<Arc<Language>>,
language_registry: &Arc<LanguageRegistry>,
) -> Result<Completion> {
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<CodeAction> {
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 {

View File

@ -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(

View File

@ -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},

View File

@ -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<Completion>;
type Response = Vec<CoreCompletion>;
type LspRequest = lsp::request::Completion;
type ProtoRequest = proto::GetCompletions;
@ -1458,9 +1457,9 @@ impl LspCommand for GetCompletions {
buffer: Model<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncAppContext,
) -> Result<Vec<Completion>> {
) -> Result<Self::Response> {
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<Completion>,
completions: Vec<CoreCompletion>,
_: &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>,
_project: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Vec<Completion>> {
) -> Result<Self::Response> {
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<BufferId> {
@ -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()
}

View File

@ -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<Anchor>,
/// 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<Documentation>,
/// 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<Anchor>,
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<Anchor>,
/// 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<Unclipped<PointUtf16>>,
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<Self>) -> Task<Result<Vec<Symbol>>> {
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::<Vec<_>>()
})?;
symbols = futures::future::join_all(new_symbols)
.await
.into_iter()
.filter_map(|symbol| symbol.log_err())
.collect::<Vec<_>>();
}
let core_symbols = response
.symbols
.into_iter()
.filter_map(|symbol| Self::deserialize_symbol(symbol).log_err())
.collect::<Vec<_>>();
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<Self>,
) -> Task<Result<Vec<Completion>>> {
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<T: ToOffset + ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
@ -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<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
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<proto::ApplyCodeActionResponse> {
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<Output = Result<Symbol>> {
let languages = self.languages.clone();
fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
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<CoreCompletion> {
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<CodeAction> {
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<Self>,
envelope: TypedEnvelope<proto::BufferSaved>,
@ -9697,6 +9836,114 @@ impl Project {
}
}
async fn populate_labels_for_symbols(
symbols: Vec<CoreSymbol>,
language_registry: &Arc<LanguageRegistry>,
default_language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>,
output: &mut Vec<Symbol>,
) {
let mut symbols_by_language = HashMap::<Option<Arc<Language>>, Vec<CoreSymbol>>::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<CoreCompletion>,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>,
completions: &mut Vec<Completion>,
) {
let lsp_completions = new_completions
.iter_mut()
.map(|completion| mem::take(&mut completion.lsp_completion))
.collect::<Vec<_>>();
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<String, bool>) -> Vec<lsp::CodeActionKind> {
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<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {