summaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
authorPoliorcetics2023-03-11 02:32:14 +0000
committerGitHub2023-03-11 02:32:14 +0000
commitbdcd4d9411655ab69245d803e88f88cc278127da (patch)
tree3131cca198bec2520a2fccc7d4c47cd3d4eddedf /helix-view
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-view')
-rw-r--r--helix-view/src/document.rs148
-rw-r--r--helix-view/src/editor.rs16
-rw-r--r--helix-view/src/tree.rs11
-rw-r--r--helix-view/src/view.rs51
4 files changed, 211 insertions, 15 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)]
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(