From bdcd4d9411655ab69245d803e88f88cc278127da Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sat, 11 Mar 2023 03:32:14 +0100 Subject: 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--- helix-view/src/document.rs | 148 +++++++++++++++++++++++++++++++++++++++++++-- helix-view/src/editor.rs | 16 +++++ helix-view/src/tree.rs | 11 ++-- helix-view/src/view.rs | 51 ++++++++++++++-- 4 files changed, 211 insertions(+), 15 deletions(-) (limited to 'helix-view/src') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b2a9ddec..19220f28 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -6,7 +6,7 @@ use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; use helix_core::doc_formatter::TextFormat; use helix_core::syntax::Highlight; -use helix_core::text_annotations::TextAnnotations; +use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; @@ -19,6 +19,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; +use std::rc::Rc; use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::SystemTime; @@ -119,6 +120,14 @@ pub struct Document { text: Rope, selections: HashMap, + /// Inlay hints annotations for the document, by view. + /// + /// To know if they're up-to-date, check the `id` field in `DocumentInlayHints`. + pub(crate) inlay_hints: HashMap, + /// Set to `true` when the document is updated, reset to `false` on the next inlay hints + /// update from the LSP + pub inlay_hints_oudated: bool, + path: Option, encoding: &'static encoding::Encoding, @@ -162,6 +171,73 @@ pub struct Document { version_control_head: Option>>>, } +/// Inlay hints for a single `(Document, View)` combo. +/// +/// There are `*_inlay_hints` field for each kind of hints an LSP can send since we offer the +/// option to style theme differently in the theme according to the (currently supported) kinds +/// (`type`, `parameter` and the rest). +/// +/// Inlay hints are always `InlineAnnotation`s, not overlays or line-ones: LSP may choose to place +/// them anywhere in the text and will sometime offer config options to move them where the user +/// wants them but it shouldn't be Helix who decides that so we use the most precise positioning. +/// +/// The padding for inlay hints needs to be stored separately for before and after (the LSP spec +/// uses 'left' and 'right' but not all text is left to right so let's be correct) padding because +/// the 'before' padding must be added to a layer *before* the regular inlay hints and the 'after' +/// padding comes ... after. +#[derive(Debug, Clone)] +pub struct DocumentInlayHints { + /// Identifier for the inlay hints stored in this structure. To be checked to know if they have + /// to be recomputed on idle or not. + pub id: DocumentInlayHintsId, + + /// Inlay hints of `TYPE` kind, if any. + pub type_inlay_hints: Rc<[InlineAnnotation]>, + + /// Inlay hints of `PARAMETER` kind, if any. + pub parameter_inlay_hints: Rc<[InlineAnnotation]>, + + /// Inlay hints that are neither `TYPE` nor `PARAMETER`. + /// + /// LSPs are not required to associate a kind to their inlay hints, for example Rust-Analyzer + /// currently never does (February 2023) and the LSP spec may add new kinds in the future that + /// we want to display even if we don't have some special highlighting for them. + pub other_inlay_hints: Rc<[InlineAnnotation]>, + + /// Inlay hint padding. When creating the final `TextAnnotations`, the `before` padding must be + /// added first, then the regular inlay hints, then the `after` padding. + pub padding_before_inlay_hints: Rc<[InlineAnnotation]>, + pub padding_after_inlay_hints: Rc<[InlineAnnotation]>, +} + +impl DocumentInlayHints { + /// Generate an empty list of inlay hints with the given ID. + pub fn empty_with_id(id: DocumentInlayHintsId) -> Self { + Self { + id, + type_inlay_hints: Rc::new([]), + parameter_inlay_hints: Rc::new([]), + other_inlay_hints: Rc::new([]), + padding_before_inlay_hints: Rc::new([]), + padding_after_inlay_hints: Rc::new([]), + } + } +} + +/// Associated with a [`Document`] and [`ViewId`], uniquely identifies the state of inlay hints for +/// for that document and view: if this changed since the last save, the inlay hints for the view +/// should be recomputed. +/// +/// We can't store the `ViewOffset` instead of the first and last asked-for lines because if +/// softwrapping changes, the `ViewOffset` may not change while the displayed lines will. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct DocumentInlayHintsId { + /// First line for which the inlay hints were requested. + pub first_line: usize, + /// Last line for which the inlay hints were requested. + pub last_line: usize, +} + use std::{fmt, mem}; impl fmt::Debug for Document { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -169,6 +245,8 @@ impl fmt::Debug for Document { .field("id", &self.id) .field("text", &self.text) .field("selections", &self.selections) + .field("inlay_hints_oudated", &self.inlay_hints_oudated) + .field("text_annotations", &self.inlay_hints) .field("path", &self.path) .field("encoding", &self.encoding) .field("restore_cursor", &self.restore_cursor) @@ -187,6 +265,15 @@ impl fmt::Debug for Document { } } +impl fmt::Debug for DocumentInlayHintsId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Much more agreable to read when debugging + f.debug_struct("DocumentInlayHintsId") + .field("lines", &(self.first_line..self.last_line)) + .finish() + } +} + // The documentation and implementation of this function should be up-to-date with // its sibling function, `to_writer()`. // @@ -389,6 +476,8 @@ impl Document { encoding, text, selections: HashMap::default(), + inlay_hints: HashMap::default(), + inlay_hints_oudated: false, indent_style: DEFAULT_INDENT, line_ending: DEFAULT_LINE_ENDING, restore_cursor: false, @@ -819,13 +908,16 @@ impl Document { } } - /// Remove a view's selection from this document. + /// Remove a view's selection and inlay hints from this document. pub fn remove_view(&mut self, view_id: ViewId) { self.selections.remove(&view_id); + self.inlay_hints.remove(&view_id); } /// Apply a [`Transaction`] to the [`Document`] to change its text. fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { + use helix_core::Assoc; + let old_doc = self.text().clone(); let success = transaction.changes().apply(&mut self.text); @@ -881,10 +973,10 @@ impl Document { .unwrap(); } + let changes = transaction.changes(); + // map state.diagnostics over changes::map_pos too for diagnostic in &mut self.diagnostics { - use helix_core::Assoc; - let changes = transaction.changes(); diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After); diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After); diagnostic.line = self.text.char_to_line(diagnostic.range.start); @@ -892,13 +984,40 @@ impl Document { self.diagnostics .sort_unstable_by_key(|diagnostic| diagnostic.range); + // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place + let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| { + if let Some(data) = Rc::get_mut(annotations) { + for inline in data.iter_mut() { + inline.char_idx = changes.map_pos(inline.char_idx, Assoc::After); + } + } + }; + + self.inlay_hints_oudated = true; + for text_annotation in self.inlay_hints.values_mut() { + let DocumentInlayHints { + id: _, + type_inlay_hints, + parameter_inlay_hints, + other_inlay_hints, + padding_before_inlay_hints, + padding_after_inlay_hints, + } = text_annotation; + + apply_inlay_hint_changes(padding_before_inlay_hints); + apply_inlay_hint_changes(type_inlay_hints); + apply_inlay_hint_changes(parameter_inlay_hints); + apply_inlay_hint_changes(other_inlay_hints); + apply_inlay_hint_changes(padding_after_inlay_hints); + } + // emit lsp notification if let Some(language_server) = self.language_server() { let notify = language_server.text_document_did_change( self.versioned_identifier(), &old_doc, self.text(), - transaction.changes(), + changes, ); if let Some(notify) = notify { @@ -1217,6 +1336,7 @@ impl Document { &self.selections[&view_id] } + #[inline] pub fn selections(&self) -> &HashMap { &self.selections } @@ -1355,9 +1475,27 @@ impl Document { } } + /// Get the text annotations that apply to the whole document, those that do not apply to any + /// specific view. pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations { TextAnnotations::default() } + + /// Set the inlay hints for this document and `view_id`. + pub fn set_inlay_hints(&mut self, view_id: ViewId, inlay_hints: DocumentInlayHints) { + self.inlay_hints.insert(view_id, inlay_hints); + } + + /// Get the inlay hints for this document and `view_id`. + pub fn inlay_hints(&self, view_id: ViewId) -> Option<&DocumentInlayHints> { + self.inlay_hints.get(&view_id) + } + + /// Completely removes all the inlay hints saved for the document, dropping them to free memory + /// (since it often means inlay hints have been fully deactivated). + pub fn reset_all_inlay_hints(&mut self) { + self.inlay_hints = Default::default(); + } } #[derive(Clone, Debug)] diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 41aa707f..bbed58d6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -345,6 +345,8 @@ pub struct LspConfig { pub auto_signature_help: bool, /// Display docs under signature help popup pub display_signature_help_docs: bool, + /// Display inlay hints + pub display_inlay_hints: bool, } impl Default for LspConfig { @@ -354,6 +356,7 @@ impl Default for LspConfig { display_messages: false, auto_signature_help: true, display_signature_help_docs: true, + display_inlay_hints: false, } } } @@ -1133,6 +1136,19 @@ impl Editor { fn _refresh(&mut self) { let config = self.config(); + + // Reset the inlay hints annotations *before* updating the views, that way we ensure they + // will disappear during the `.sync_change(doc)` call below. + // + // We can't simply check this config when rendering because inlay hints are only parts of + // the possible annotations, and others could still be active, so we need to selectively + // drop the inlay hints. + if !config.lsp.display_inlay_hints { + for doc in self.documents_mut() { + doc.reset_all_inlay_hints(); + } + } + for (view, _) in self.tree.views_mut() { let doc = doc_mut!(self, &view.doc); view.sync_changes(doc); diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 5ec2773d..e8afd204 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -278,16 +278,15 @@ impl Tree { self.try_get(index).unwrap() } - /// Try to get reference to a [View] by index. Returns `None` if node content is not a [Content::View] - /// # Panics + /// Try to get reference to a [View] by index. Returns `None` if node content is not a [`Content::View`]. /// - /// Panics if `index` is not in self.nodes. This can be checked with [Self::contains] + /// Does not panic if the view does not exists anymore. pub fn try_get(&self, index: ViewId) -> Option<&View> { - match &self.nodes[index] { - Node { + match self.nodes.get(index) { + Some(Node { content: Content::View(view), .. - } => Some(view), + }) => Some(view), _ => None, } } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 7bfbb241..0ac7ca3b 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -1,19 +1,21 @@ use crate::{ align_view, + document::DocumentInlayHints, editor::{GutterConfig, GutterType}, graphics::Rect, Align, Document, DocumentId, Theme, ViewId, }; use helix_core::{ - char_idx_at_visual_offset, doc_formatter::TextFormat, text_annotations::TextAnnotations, - visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection, - Transaction, + char_idx_at_visual_offset, doc_formatter::TextFormat, syntax::Highlight, + text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block, + Position, RopeSlice, Selection, Transaction, }; use std::{ collections::{HashMap, VecDeque}, fmt, + rc::Rc, }; const JUMP_LIST_CAPACITY: usize = 30; @@ -402,9 +404,50 @@ impl View { Some(pos) } + /// Get the text annotations to display in the current view for the given document and theme. pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations { // TODO custom annotations for custom views like side by side diffs - doc.text_annotations(theme) + + let mut text_annotations = doc.text_annotations(theme); + + let DocumentInlayHints { + id: _, + type_inlay_hints, + parameter_inlay_hints, + other_inlay_hints, + padding_before_inlay_hints, + padding_after_inlay_hints, + } = match doc.inlay_hints.get(&self.id) { + Some(doc_inlay_hints) => doc_inlay_hints, + None => return text_annotations, + }; + + let type_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) + .map(Highlight); + let parameter_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter")) + .map(Highlight); + let other_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) + .map(Highlight); + + let mut add_annotations = |annotations: &Rc<[_]>, style| { + if !annotations.is_empty() { + text_annotations.add_inline_annotations(Rc::clone(annotations), style); + } + }; + + // Overlapping annotations are ignored apart from the first so the order here is not random: + // types -> parameters -> others should hopefully be the "correct" order for most use cases, + // with the padding coming before and after as expected. + add_annotations(padding_before_inlay_hints, None); + add_annotations(type_inlay_hints, type_style); + add_annotations(parameter_inlay_hints, parameter_style); + add_annotations(other_inlay_hints, other_style); + add_annotations(padding_after_inlay_hints, None); + + text_annotations } pub fn text_pos_at_screen_coords( -- cgit v1.2.3-70-g09d2