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-view/src/document.rs | |
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-view/src/document.rs')
-rw-r--r-- | helix-view/src/document.rs | 148 |
1 files changed, 143 insertions, 5 deletions
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<ViewId, Selection>, + /// 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<ViewId, DocumentInlayHints>, + /// 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<PathBuf>, encoding: &'static encoding::Encoding, @@ -162,6 +171,73 @@ pub struct Document { version_control_head: Option<Arc<ArcSwap<Box<str>>>>, } +/// 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<ViewId, Selection> { &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)] |