feat: add live-cycling LSP completion

This commit is contained in:
2026-10-10 18:32:39 +02:00
parent 9d3b25e9d3
commit b6059d5ed9
11 changed files with 1916 additions and 55 deletions
+75 -11
View File
@@ -41,7 +41,9 @@ pub struct EditorSession {
lsp: Lsp,
lsp_request_id: u64,
lsp_source_cursor: Option<Cursor>,
lsp_source_mode: Mode,
pending_hover: bool,
pending_completion: bool,
hover_owner: Option<String>,
}
@@ -129,7 +131,9 @@ impl EditorSession {
lsp: Lsp::new(config.lsp),
lsp_request_id: 0,
lsp_source_cursor: None,
lsp_source_mode: Mode::Normal,
pending_hover: false,
pending_completion: false,
hover_owner: None,
};
session.synchronize_documents();
@@ -141,7 +145,7 @@ impl EditorSession {
}
pub fn handle(&mut self, input: Input) {
self.cancel_pending_hover();
self.cancel_pending_popup_request();
let invocation = self.editor.handle(input);
self.synchronize_documents();
if let Some(invocation) = invocation {
@@ -151,7 +155,7 @@ impl EditorSession {
}
pub fn execute(&mut self, command: &str) {
self.cancel_pending_hover();
self.cancel_pending_popup_request();
let invocation = self.editor.execute(command);
self.synchronize_documents();
if let Some(invocation) = invocation {
@@ -163,7 +167,7 @@ impl EditorSession {
fn dispatch(&mut self, invocation: Invocation) {
if invocation.plugin == "core" {
let result = match invocation.command.as_str() {
"lsp.definition" | "lsp.hover" => {
"lsp.definition" | "lsp.hover" | "lsp.completion" => {
if let Some(id) = self.lsp_request_id.checked_add(1) {
self.lsp_request_id = id;
let context = self.editor.context();
@@ -173,11 +177,13 @@ impl EditorSession {
};
self.requests.entry("core".into()).or_default().latest = Some((id, source));
self.lsp_source_cursor = Some(self.editor.cursor());
self.lsp_source_mode = self.editor.mode();
self.pending_hover = invocation.command == "lsp.hover";
if self.pending_hover {
self.lsp.hover(&context, id)
} else {
self.lsp.definition(&context, id)
self.pending_completion = invocation.command == "lsp.completion";
match invocation.command.as_str() {
"lsp.hover" => self.lsp.hover(&context, id),
"lsp.completion" => self.lsp.completion(&context, id),
_ => self.lsp.definition(&context, id),
}
} else {
Err("LSP request IDs exhausted".into())
@@ -187,6 +193,7 @@ impl EditorSession {
_ => unreachable!("session-dispatched core command"),
};
if let Err(error) = result {
self.cancel_pending_popup_request();
self.editor.set_message(format!("LSP: {error}"));
}
return;
@@ -216,12 +223,13 @@ impl EditorSession {
}
}
fn cancel_pending_hover(&mut self) {
if self.pending_hover {
fn cancel_pending_popup_request(&mut self) {
if self.pending_hover || self.pending_completion {
if let Some(requests) = self.requests.get_mut("core") {
requests.latest = None;
}
self.pending_hover = false;
self.pending_completion = false;
}
}
@@ -243,12 +251,13 @@ impl EditorSession {
source.buffer_id != self.editor.buffer_id()
|| source.revision != self.editor.text_revision()
|| self.lsp_source_cursor != Some(self.editor.cursor())
|| self.editor.mode() != Mode::Normal
|| self.editor.mode() != self.lsp_source_mode
})
{
requests.latest = None;
self.lsp_source_cursor = None;
self.pending_hover = false;
self.pending_completion = false;
}
if !self.lsp.is_enabled() && !self.plugins.values().any(RubyPlugin::is_async) {
self.documents.clear();
@@ -363,7 +372,7 @@ impl EditorSession {
&& self.source_is_current(source)
&& (owner != "core"
|| (self.lsp_source_cursor == Some(self.editor.cursor())
&& self.editor.mode() == Mode::Normal))
&& self.editor.mode() == self.lsp_source_mode))
&& self
.requests
.get(owner)
@@ -390,6 +399,18 @@ impl EditorSession {
.is_none_or(|picker| !picker.focused)
}
fn completion_is_current(&self, request_id: Option<u64>, source: InvocationSource) -> bool {
self.pending_completion
&& self.request_is_current("core", request_id, source)
&& self.editor.mode() == Mode::Insert
&& self.editor.palette().is_none()
&& self
.editor
.file_picker()
.is_none_or(|picker| !picker.focused)
&& self.editor.startup_diagnostic().is_none()
}
fn apply_update(&mut self, owner: &str, update: PluginUpdate) -> Result<bool, String> {
if owner == "core"
&& let Some(id) = update.request_id
@@ -402,6 +423,30 @@ impl EditorSession {
{
return Ok(false);
}
for effect in &update.effects {
if let UpdateEffect::Completion {
buffer_id,
revision,
items,
} = effect
{
if owner != "core" {
return Err("Completion is a native LSP update only".into());
}
let source = InvocationSource {
buffer_id: *buffer_id,
revision: *revision,
};
if self.completion_is_current(update.request_id, source)
&& let Err(error) = self.editor.validate_completion(items)
{
self.pending_completion = false;
self.editor
.set_message(format!("LSP completion failed: {error}"));
return Ok(true);
}
}
}
// Validate the whole applicable annotation batch before changing any buffer.
let validation = (|| -> Result<(), String> {
for effect in &update.effects {
@@ -460,6 +505,7 @@ impl EditorSession {
crate::editor::validate_hover_text(text)?;
}
}
UpdateEffect::Completion { .. } => {}
UpdateEffect::Message { .. } => {}
}
}
@@ -531,6 +577,24 @@ impl EditorSession {
changed = true;
}
}
UpdateEffect::Completion {
buffer_id,
revision,
items,
} => {
let source = InvocationSource {
buffer_id,
revision,
};
if self.completion_is_current(update.request_id, source) {
self.pending_completion = false;
if let Err(error) = self.editor.show_completion(items) {
self.editor
.set_message(format!("LSP completion failed: {error}"));
}
changed = true;
}
}
UpdateEffect::Message { text } => {
changed |= self.editor.message() != text;
self.editor.set_message(text);