aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorPoliorcetics2023-03-11 02:32:14 +0000
committerGitHub2023-03-11 02:32:14 +0000
commitbdcd4d9411655ab69245d803e88f88cc278127da (patch)
tree3131cca198bec2520a2fccc7d4c47cd3d4eddedf /helix-term/src
parent3d230e701d4771377a6b3f3b8c68527af29ee066 (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/src')
-rw-r--r--helix-term/src/commands.rs33
-rw-r--r--helix-term/src/commands/lsp.rs183
-rw-r--r--helix-term/src/ui/editor.rs6
-rw-r--r--helix-term/src/ui/picker.rs4
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,