feat: add native LSP hover action and popup

This commit is contained in:
Valdemar Lorenzen
2026-10-09 14:29:58 +02:00
parent 529d1fd5b3
commit f093091002
10 changed files with 842 additions and 49 deletions
+86 -5
View File
@@ -41,6 +41,8 @@ pub struct EditorSession {
lsp: Lsp,
lsp_request_id: u64,
lsp_source_cursor: Option<Cursor>,
pending_hover: bool,
hover_owner: Option<String>,
}
impl EditorSession {
@@ -124,6 +126,8 @@ impl EditorSession {
lsp: Lsp::new(config.lsp),
lsp_request_id: 0,
lsp_source_cursor: None,
pending_hover: false,
hover_owner: None,
};
session.synchronize_documents();
Ok(session)
@@ -134,6 +138,7 @@ impl EditorSession {
}
pub fn handle(&mut self, input: Input) {
self.cancel_pending_hover();
let invocation = self.editor.handle(input);
self.synchronize_documents();
if let Some(invocation) = invocation {
@@ -143,6 +148,7 @@ impl EditorSession {
}
pub fn execute(&mut self, command: &str) {
self.cancel_pending_hover();
let invocation = self.editor.execute(command);
self.synchronize_documents();
if let Some(invocation) = invocation {
@@ -154,7 +160,7 @@ impl EditorSession {
fn dispatch(&mut self, invocation: Invocation) {
if invocation.plugin == "core" {
let result = match invocation.command.as_str() {
"lsp_definition" => {
"lsp_definition" | "lsp_hover" => {
if let Some(id) = self.lsp_request_id.checked_add(1) {
self.lsp_request_id = id;
let context = self.editor.context();
@@ -164,7 +170,12 @@ impl EditorSession {
};
self.requests.entry("core".into()).or_default().latest = Some((id, source));
self.lsp_source_cursor = Some(self.editor.cursor());
self.lsp.definition(&context, id)
self.pending_hover = invocation.command == "lsp_hover";
if self.pending_hover {
self.lsp.hover(&context, id)
} else {
self.lsp.definition(&context, id)
}
} else {
Err("LSP request IDs exhausted".into())
}
@@ -202,11 +213,24 @@ impl EditorSession {
}
}
fn cancel_pending_hover(&mut self) {
if self.pending_hover {
if let Some(requests) = self.requests.get_mut("core") {
requests.latest = None;
}
self.pending_hover = false;
}
}
fn retire(&mut self, owner: &str, error: &str) {
// Drop also stops a live host whose protocol-valid effects were rejected.
drop(self.plugins.remove(owner));
self.requests.remove(owner);
self.editor.disable_plugin(owner, error);
if self.hover_owner.as_deref() == Some(owner) {
self.editor.clear_hover();
self.hover_owner = None;
}
}
fn synchronize_documents(&mut self) -> bool {
@@ -313,7 +337,7 @@ impl EditorSession {
.any(|document| document.id == source.buffer_id && document.revision == source.revision)
}
fn navigation_is_current(
fn request_is_current(
&self,
owner: &str,
request_id: Option<u64>,
@@ -335,7 +359,33 @@ impl EditorSession {
})
}
fn hover_is_current(
&self,
owner: &str,
request_id: Option<u64>,
source: InvocationSource,
) -> bool {
self.request_is_current(owner, request_id, source)
&& self.editor.mode() == Mode::Normal
&& self.editor.palette().is_none()
&& self
.editor
.file_picker()
.is_none_or(|picker| !picker.focused)
}
fn apply_update(&mut self, owner: &str, update: PluginUpdate) -> Result<bool, String> {
if owner == "core"
&& let Some(id) = update.request_id
&& self
.requests
.get(owner)
.and_then(|requests| requests.latest)
.map(|latest| latest.0)
!= Some(id)
{
return Ok(false);
}
// Validate the whole applicable annotation batch before changing any buffer.
for effect in &update.effects {
match effect {
@@ -357,7 +407,7 @@ impl EditorSession {
buffer_id: *buffer_id,
revision: *revision,
};
if self.navigation_is_current(owner, update.request_id, source)
if self.request_is_current(owner, update.request_id, source)
&& (path.as_os_str().is_empty()
|| path.to_string_lossy().chars().any(char::is_control))
{
@@ -366,6 +416,19 @@ impl EditorSession {
);
}
}
UpdateEffect::Hover {
buffer_id,
revision,
text,
} => {
let source = InvocationSource {
buffer_id: *buffer_id,
revision: *revision,
};
if self.hover_is_current(owner, update.request_id, source) {
crate::editor::validate_hover_text(text)?;
}
}
UpdateEffect::Message { .. } => {}
}
}
@@ -392,7 +455,7 @@ impl EditorSession {
buffer_id,
revision,
};
if self.navigation_is_current(owner, update.request_id, source) {
if self.request_is_current(owner, update.request_id, source) {
if let Err(error) =
self.editor.navigate_to(path, TextPosition { row, column })
{
@@ -402,6 +465,24 @@ impl EditorSession {
changed = true;
}
}
UpdateEffect::Hover {
buffer_id,
revision,
text,
} => {
let source = InvocationSource {
buffer_id,
revision,
};
if self.hover_is_current(owner, update.request_id, source) {
self.editor.show_hover(text)?;
self.hover_owner = Some(owner.into());
if owner == "core" {
self.pending_hover = false;
}
changed = true;
}
}
UpdateEffect::Message { text } => {
changed |= self.editor.message() != text;
self.editor.set_message(text);