Support word-based completions (#26410)
Closes https://github.com/zed-industries/zed/issues/4957 https://github.com/user-attachments/assets/ff491378-376d-48ec-b552-6cc80f74200b Adds `"completions"` language settings section, to configure LSP and word completions per language. Word-based completions may be turned on never, always (returned along with the LSP ones), and as a fallback if no LSP completion items were returned. Future work: * words are matched with the same fuzzy matching code that the rest of the completions are This might worsen the completion menu's usability even more, and will require work on better completion sorting. * completion entries currently have no icons or other ways to indicate those are coming from LSP or from word search, or from something else * we may work with language scopes more intelligently, group words by them and distinguish during completions Release Notes: - Supported word-based completions --------- Co-authored-by: Max Brunsfeld <max@zed.dev>
This commit is contained in:
parent
74c29f1818
commit
91c209900b
10 changed files with 632 additions and 102 deletions
|
@ -23,13 +23,13 @@ use client::{proto, TypedEnvelope};
|
|||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use futures::{
|
||||
future::{join_all, Shared},
|
||||
select,
|
||||
select, select_biased,
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||
};
|
||||
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
|
@ -4325,6 +4325,15 @@ impl LspStore {
|
|||
let offset = position.to_offset(&snapshot);
|
||||
let scope = snapshot.language_scope_at(offset);
|
||||
let language = snapshot.language().cloned();
|
||||
let completion_settings = language_settings(
|
||||
language.as_ref().map(|language| language.name()),
|
||||
buffer.read(cx).file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
if !completion_settings.lsp {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
|
||||
local
|
||||
|
@ -4341,23 +4350,51 @@ impl LspStore {
|
|||
});
|
||||
|
||||
let buffer = buffer.clone();
|
||||
let lsp_timeout = completion_settings.lsp_fetch_timeout_ms;
|
||||
let lsp_timeout = if lsp_timeout > 0 {
|
||||
Some(Duration::from_millis(lsp_timeout))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let mut tasks = Vec::with_capacity(server_ids.len());
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |lsp_store, cx| {
|
||||
for server_id in server_ids {
|
||||
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,
|
||||
context: context.clone(),
|
||||
let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id);
|
||||
let lsp_timeout = lsp_timeout
|
||||
.map(|lsp_timeout| cx.background_executor().timer(lsp_timeout));
|
||||
let mut timeout = cx.background_spawn(async move {
|
||||
match lsp_timeout {
|
||||
Some(lsp_timeout) => {
|
||||
lsp_timeout.await;
|
||||
true
|
||||
},
|
||||
cx,
|
||||
),
|
||||
));
|
||||
None => false,
|
||||
}
|
||||
}).fuse();
|
||||
let mut lsp_request = lsp_store.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetCompletions {
|
||||
position,
|
||||
context: context.clone(),
|
||||
},
|
||||
cx,
|
||||
).fuse();
|
||||
let new_task = cx.background_spawn(async move {
|
||||
select_biased! {
|
||||
response = lsp_request => response,
|
||||
timeout_happened = timeout => {
|
||||
if timeout_happened {
|
||||
log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms);
|
||||
return anyhow::Ok(Vec::new())
|
||||
} else {
|
||||
lsp_request.await
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
tasks.push((lsp_adapter, new_task));
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -4416,47 +4453,58 @@ impl LspStore {
|
|||
{
|
||||
did_resolve = true;
|
||||
}
|
||||
} else {
|
||||
resolve_word_completion(
|
||||
&buffer_snapshot,
|
||||
&mut completions.borrow_mut()[completion_index],
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let Some(server_id) = completions.borrow()[completion_index].source.server_id()
|
||||
else {
|
||||
continue;
|
||||
let server_id = {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
completion.source.server_id()
|
||||
};
|
||||
if let Some(server_id) = server_id {
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
let server = lsp_store.language_server_for_id(server_id)?;
|
||||
let adapter =
|
||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||
Some((server, adapter))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((server, adapter)) = server_and_adapter else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
let server = lsp_store.language_server_for_id(server_id)?;
|
||||
let adapter =
|
||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||
Some((server, adapter))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((server, adapter)) = server_and_adapter else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let resolved = Self::resolve_completion_local(
|
||||
server,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some();
|
||||
if resolved {
|
||||
Self::regenerate_completion_labels(
|
||||
adapter,
|
||||
let resolved = Self::resolve_completion_local(
|
||||
server,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
did_resolve = true;
|
||||
.log_err()
|
||||
.is_some();
|
||||
if resolved {
|
||||
Self::regenerate_completion_labels(
|
||||
adapter,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
did_resolve = true;
|
||||
}
|
||||
} else {
|
||||
resolve_word_completion(
|
||||
&buffer_snapshot,
|
||||
&mut completions.borrow_mut()[completion_index],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4500,7 +4548,9 @@ impl LspStore {
|
|||
);
|
||||
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let resolved_completion = request.await?;
|
||||
|
@ -4641,7 +4691,9 @@ impl LspStore {
|
|||
}
|
||||
serde_json::to_string(lsp_completion).unwrap().into_bytes()
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let request = proto::ResolveCompletionDocumentation {
|
||||
|
@ -8172,51 +8224,54 @@ impl LspStore {
|
|||
}
|
||||
|
||||
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
||||
let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
|
||||
let mut serialized_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(),
|
||||
..proto::Completion::default()
|
||||
};
|
||||
match &completion.source {
|
||||
CompletionSource::Lsp {
|
||||
server_id,
|
||||
lsp_completion,
|
||||
lsp_defaults,
|
||||
resolved,
|
||||
} => (
|
||||
proto::completion::Source::Lsp as i32,
|
||||
server_id.0 as u64,
|
||||
serde_json::to_vec(lsp_completion).unwrap(),
|
||||
lsp_defaults
|
||||
} => {
|
||||
serialized_completion.source = proto::completion::Source::Lsp as i32;
|
||||
serialized_completion.server_id = server_id.0 as u64;
|
||||
serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap();
|
||||
serialized_completion.lsp_defaults = lsp_defaults
|
||||
.as_deref()
|
||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
|
||||
*resolved,
|
||||
),
|
||||
CompletionSource::Custom => (
|
||||
proto::completion::Source::Custom as i32,
|
||||
0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
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,
|
||||
lsp_completion,
|
||||
lsp_defaults,
|
||||
resolved,
|
||||
source,
|
||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap());
|
||||
serialized_completion.resolved = *resolved;
|
||||
}
|
||||
CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved,
|
||||
} => {
|
||||
serialized_completion.source = proto::completion::Source::BufferWord as i32;
|
||||
serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start));
|
||||
serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end));
|
||||
serialized_completion.resolved = *resolved;
|
||||
}
|
||||
CompletionSource::Custom => {
|
||||
serialized_completion.source = proto::completion::Source::Custom as i32;
|
||||
serialized_completion.resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
serialized_completion
|
||||
}
|
||||
|
||||
pub(crate) 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"))?;
|
||||
.context("invalid old start")?;
|
||||
let old_end = completion
|
||||
.old_end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
||||
.context("invalid old end")?;
|
||||
Ok(CoreCompletion {
|
||||
old_range: old_start..old_end,
|
||||
new_text: completion.new_text,
|
||||
|
@ -8232,6 +8287,20 @@ impl LspStore {
|
|||
.transpose()?,
|
||||
resolved: completion.resolved,
|
||||
},
|
||||
Some(proto::completion::Source::BufferWord) => {
|
||||
let word_range = completion
|
||||
.buffer_word_start
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid buffer word start")?
|
||||
..completion
|
||||
.buffer_word_end
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid buffer word end")?;
|
||||
CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: completion.resolved,
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
||||
},
|
||||
})
|
||||
|
@ -8296,6 +8365,40 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
|
||||
let CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved,
|
||||
} = &mut completion.source
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *resolved {
|
||||
return;
|
||||
}
|
||||
|
||||
if completion.new_text
|
||||
!= snapshot
|
||||
.text_for_range(word_range.clone())
|
||||
.collect::<String>()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
for chunk in snapshot.chunks(word_range.clone(), true) {
|
||||
let end_offset = offset + chunk.text.len();
|
||||
if let Some(highlight_id) = chunk.syntax_highlight_id {
|
||||
completion
|
||||
.label
|
||||
.runs
|
||||
.push((offset..end_offset, highlight_id));
|
||||
}
|
||||
offset = end_offset;
|
||||
}
|
||||
*resolved = true;
|
||||
}
|
||||
|
||||
impl EventEmitter<LspStoreEvent> for LspStore {}
|
||||
|
||||
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue