diff options
author | Poliorcetics | 2023-03-11 02:32:14 +0000 |
---|---|---|
committer | GitHub | 2023-03-11 02:32:14 +0000 |
commit | bdcd4d9411655ab69245d803e88f88cc278127da (patch) | |
tree | 3131cca198bec2520a2fccc7d4c47cd3d4eddedf /helix-term | |
parent | 3d230e701d4771377a6b3f3b8c68527af29ee066 (diff) |
Feat: LSP Type Hints (#5934)
* misc: missing inline, outdated link
* doc: Add new theme keys and config option to book
* fix: don't panic in Tree::try_get(view_id)
Necessary for later, where we could be receiving an LSP response
for a closed window, in which case we don't want to crash while
checking for its existence
* fix: reset idle timer on all mouse events
* refacto: Introduce Overlay::new and InlineAnnotation::new
* refacto: extract make_job_callback from Context::callback
* feat: add LSP display_inlay_hint option to config
* feat: communicate inlay hints support capabilities of helix to LSP server
* feat: Add function to request range of inlay hint from LSP
* feat: Save inlay hints in document, per view
* feat: Update inlay hints on document changes
* feat: Compute inlay hints on idle timeout
* nit: Add todo's about inlay hints for later
* fix: compute text annotations for current view in view.rs, not document.rs
* doc: Improve Document::text_annotations() description
* nit: getters don't use 'get_' in front
* fix: Drop inlay hints annotations on config refresh if necessary
* fix: padding theming for LSP inlay hints
* fix: tracking of outdated inlay hints should not be dependant on document revision (because of undos and such)
* fix: follow LSP spec and don't highlight padding as virtual text
* config: add some LSP inlay hint configs
Diffstat (limited to 'helix-term')
-rw-r--r-- | helix-term/src/commands.rs | 33 | ||||
-rw-r--r-- | helix-term/src/commands/lsp.rs | 183 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 6 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 4 |
4 files changed, 212 insertions, 14 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 803f4051..1c1edece 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -114,17 +114,7 @@ impl<'a> Context<'a> { T: for<'de> serde::Deserialize<'de> + Send + 'static, F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static, { - let callback = Box::pin(async move { - let json = call.await?; - let response = serde_json::from_value(json)?; - let call: job::Callback = Callback::EditorCompositor(Box::new( - move |editor: &mut Editor, compositor: &mut Compositor| { - callback(editor, compositor, response) - }, - )); - Ok(call) - }); - self.jobs.callback(callback); + self.jobs.callback(make_job_callback(call, callback)); } /// Returns 1 if no explicit count was provided @@ -134,6 +124,27 @@ impl<'a> Context<'a> { } } +#[inline] +fn make_job_callback<T, F>( + call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, + callback: F, +) -> std::pin::Pin<Box<impl Future<Output = Result<Callback, anyhow::Error>>>> +where + T: for<'de> serde::Deserialize<'de> + Send + 'static, + F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static, +{ + Box::pin(async move { + let json = call.await?; + let response = serde_json::from_value(json)?; + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + callback(editor, compositor, response) + }, + )); + Ok(call) + }) +} + use helix_view::{align_view, Align}; /// A MappableCommand is either a static command like "jump_view_up" or a Typable command like diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 08519366..f9d9856f 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -15,8 +15,13 @@ use tui::{ use super::{align_view, push_jump, Align, Context, Editor, Open}; -use helix_core::{path, Selection}; -use helix_view::{document::Mode, editor::Action, theme::Style}; +use helix_core::{path, text_annotations::InlineAnnotation, Selection}; +use helix_view::{ + document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, + editor::Action, + theme::Style, + Document, View, +}; use crate::{ compositor::{self, Compositor}, @@ -27,7 +32,8 @@ use crate::{ }; use std::{ - borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, path::PathBuf, sync::Arc, + borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, + sync::Arc, }; /// Gets the language server that is attached to a document, and @@ -1391,3 +1397,174 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { }, ); } + +pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) { + if !editor.config().lsp.display_inlay_hints { + return; + } + + for (view, _) in editor.tree.views() { + let doc = match editor.documents.get(&view.doc) { + Some(doc) => doc, + None => continue, + }; + if let Some(callback) = compute_inlay_hints_for_view(view, doc) { + jobs.callback(callback); + } + } +} + +fn compute_inlay_hints_for_view( + view: &View, + doc: &Document, +) -> Option<std::pin::Pin<Box<impl Future<Output = Result<crate::job::Callback, anyhow::Error>>>>> { + let view_id = view.id; + let doc_id = view.doc; + + let language_server = doc.language_server()?; + + let capabilities = language_server.capabilities(); + + let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider { + Some( + lsp::OneOf::Left(true) + | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)), + ) => { + let doc_text = doc.text(); + let len_lines = doc_text.len_lines(); + + // Compute ~3 times the current view height of inlay hints, that way some scrolling + // will not show half the view with hints and half without while still being faster + // than computing all the hints for the full file (which could be dozens of time + // longer than the view is). + let view_height = view.inner_height(); + let first_visible_line = doc_text.char_to_line(view.offset.anchor); + let first_line = first_visible_line.saturating_sub(view_height); + let last_line = first_visible_line + .saturating_add(view_height.saturating_mul(2)) + .min(len_lines); + + let new_doc_inlay_hint_id = DocumentInlayHintsId { + first_line, + last_line, + }; + // Don't recompute the annotations in case nothing has changed about the view + if !doc.inlay_hints_oudated + && doc + .inlay_hints(view_id) + .map_or(false, |dih| dih.id == new_doc_inlay_hint_id) + { + return None; + } + + let doc_slice = doc_text.slice(..); + let first_char_in_range = doc_slice.line_to_char(first_line); + let last_char_in_range = doc_slice.line_to_char(last_line); + + let range = helix_lsp::util::range_to_lsp_range( + doc_text, + helix_core::Range::new(first_char_in_range, last_char_in_range), + language_server.offset_encoding(), + ); + + ( + language_server.text_document_range_inlay_hints(doc.identifier(), range, None), + new_doc_inlay_hint_id, + ) + } + _ => return None, + }; + + let callback = super::make_job_callback( + future?, + move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| { + // The config was modified or the window was closed while the request was in flight + if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() { + return; + } + + // Add annotations to relevant document, not the current one (it may have changed in between) + let doc = match editor.documents.get_mut(&doc_id) { + Some(doc) => doc, + None => return, + }; + + // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated + let (mut hints, offset_encoding) = match (response, doc.language_server()) { + (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()), + _ => { + doc.set_inlay_hints( + view_id, + DocumentInlayHints::empty_with_id(new_doc_inlay_hints_id), + ); + doc.inlay_hints_oudated = false; + return; + } + }; + + // Most language servers will already send them sorted but ensure this is the case to + // avoid errors on our end. + hints.sort_unstable_by_key(|inlay_hint| inlay_hint.position); + + let mut padding_before_inlay_hints = Vec::new(); + let mut type_inlay_hints = Vec::new(); + let mut parameter_inlay_hints = Vec::new(); + let mut other_inlay_hints = Vec::new(); + let mut padding_after_inlay_hints = Vec::new(); + + let doc_text = doc.text(); + + for hint in hints { + let char_idx = + match helix_lsp::util::lsp_pos_to_pos(doc_text, hint.position, offset_encoding) + { + Some(pos) => pos, + // Skip inlay hints that have no "real" position + None => continue, + }; + + let label = match hint.label { + lsp::InlayHintLabel::String(s) => s, + lsp::InlayHintLabel::LabelParts(parts) => parts + .into_iter() + .map(|p| p.value) + .collect::<Vec<_>>() + .join(""), + }; + + let inlay_hints_vec = match hint.kind { + Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints, + Some(lsp::InlayHintKind::PARAMETER) => &mut parameter_inlay_hints, + // We can't warn on unknown kind here since LSPs are free to set it or not, for + // example Rust Analyzer does not: every kind will be `None`. + _ => &mut other_inlay_hints, + }; + + if let Some(true) = hint.padding_left { + padding_before_inlay_hints.push(InlineAnnotation::new(char_idx, " ")); + } + + inlay_hints_vec.push(InlineAnnotation::new(char_idx, label)); + + if let Some(true) = hint.padding_right { + padding_after_inlay_hints.push(InlineAnnotation::new(char_idx, " ")); + } + } + + doc.set_inlay_hints( + view_id, + DocumentInlayHints { + id: new_doc_inlay_hints_id, + type_inlay_hints: type_inlay_hints.into(), + parameter_inlay_hints: parameter_inlay_hints.into(), + other_inlay_hints: other_inlay_hints.into(), + padding_before_inlay_hints: padding_before_inlay_hints.into(), + padding_after_inlay_hints: padding_after_inlay_hints.into(), + }, + ); + doc.inlay_hints_oudated = false; + }, + ); + + Some(callback) +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4abbe01e..7c22df74 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -990,6 +990,8 @@ impl EditorView { } pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult { + commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs); + if let Some(completion) = &mut self.completion { return if completion.ensure_item_resolved(cx) { EventResult::Consumed(None) @@ -1014,6 +1016,10 @@ impl EditorView { event: &MouseEvent, cxt: &mut commands::Context, ) -> EventResult { + if event.kind != MouseEventKind::Moved { + cxt.editor.reset_idle_timer(); + } + let config = cxt.editor.config(); let MouseEvent { kind, diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ec8b1c7f..bc2f98ee 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -225,6 +225,9 @@ impl<T: Item> FilePicker<T> { let loader = cx.editor.syn_loader.clone(); doc.detect_language(loader); } + + // QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now + // but it could be interesting in the future } EventResult::Consumed(None) @@ -339,6 +342,7 @@ impl<T: Item + 'static> Component for FilePicker<T> { inner, doc, offset, + // TODO: compute text annotations asynchronously here (like inlay hints) &TextAnnotations::default(), highlights, &cx.editor.theme, |