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

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