summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--book/src/configuration.md3
-rw-r--r--book/src/themes.md109
-rw-r--r--helix-core/src/diagnostic.rs2
-rw-r--r--helix-core/src/doc_formatter/test.rs56
-rw-r--r--helix-core/src/text_annotations.rs43
-rw-r--r--helix-lsp/src/client.rs32
-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
-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
-rw-r--r--languages.toml72
15 files changed, 618 insertions, 151 deletions
diff --git a/book/src/configuration.md b/book/src/configuration.md
index e698646b..ec692cab 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -120,9 +120,12 @@ The following statusline elements can be configured:
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
+| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
+[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix.
+ Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances, please report any bugs you see so we can fix them!
### `[editor.cursor-shape]` Section
diff --git a/book/src/themes.md b/book/src/themes.md
index 5ddd4f2c..929f821e 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -262,58 +262,61 @@ These scopes are used for theming the editor interface:
- `hover` - for hover popup UI
-| Key | Notes |
-| --- | --- |
-| `ui.background` | |
-| `ui.background.separator` | Picker separator below input line |
-| `ui.cursor` | |
-| `ui.cursor.normal` | |
-| `ui.cursor.insert` | |
-| `ui.cursor.select` | |
-| `ui.cursor.match` | Matching bracket etc. |
-| `ui.cursor.primary` | Cursor with primary selection |
-| `ui.cursor.primary.normal` | |
-| `ui.cursor.primary.insert` | |
-| `ui.cursor.primary.select` | |
-| `ui.gutter` | Gutter |
-| `ui.gutter.selected` | Gutter for the line the cursor is on |
-| `ui.linenr` | Line numbers |
-| `ui.linenr.selected` | Line number for the line the cursor is on |
-| `ui.statusline` | Statusline |
-| `ui.statusline.inactive` | Statusline (unfocused document) |
-| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
-| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
-| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
-| `ui.statusline.separator` | Separator character in statusline |
-| `ui.popup` | Documentation popups (e.g. Space + k) |
-| `ui.popup.info` | Prompt for multiple key options |
-| `ui.window` | Borderlines separating splits |
-| `ui.help` | Description box for commands |
-| `ui.text` | Command prompts, popup text, etc. |
-| `ui.text.focus` | |
-| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
-| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
-| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
-| `ui.virtual.whitespace` | Visible whitespace characters |
-| `ui.virtual.indent-guide` | Vertical indent width guides |
-| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
-| `ui.menu` | Code and command completion menus |
-| `ui.menu.selected` | Selected autocomplete item |
-| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
-| `ui.selection` | For selections in the editing area |
-| `ui.selection.primary` | |
-| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
-| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
-| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
-| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
-| `warning` | Diagnostics warning (gutter) |
-| `error` | Diagnostics error (gutter) |
-| `info` | Diagnostics info (gutter) |
-| `hint` | Diagnostics hint (gutter) |
-| `diagnostic` | Diagnostics fallback style (editing area) |
-| `diagnostic.hint` | Diagnostics hint (editing area) |
-| `diagnostic.info` | Diagnostics info (editing area) |
-| `diagnostic.warning` | Diagnostics warning (editing area) |
-| `diagnostic.error` | Diagnostics error (editing area) |
+| Key | Notes |
+| --- | --- |
+| `ui.background` | |
+| `ui.background.separator` | Picker separator below input line |
+| `ui.cursor` | |
+| `ui.cursor.normal` | |
+| `ui.cursor.insert` | |
+| `ui.cursor.select` | |
+| `ui.cursor.match` | Matching bracket etc. |
+| `ui.cursor.primary` | Cursor with primary selection |
+| `ui.cursor.primary.normal` | |
+| `ui.cursor.primary.insert` | |
+| `ui.cursor.primary.select` | |
+| `ui.gutter` | Gutter |
+| `ui.gutter.selected` | Gutter for the line the cursor is on |
+| `ui.linenr` | Line numbers |
+| `ui.linenr.selected` | Line number for the line the cursor is on |
+| `ui.statusline` | Statusline |
+| `ui.statusline.inactive` | Statusline (unfocused document) |
+| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
+| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
+| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
+| `ui.statusline.separator` | Separator character in statusline |
+| `ui.popup` | Documentation popups (e.g. Space + k) |
+| `ui.popup.info` | Prompt for multiple key options |
+| `ui.window` | Borderlines separating splits |
+| `ui.help` | Description box for commands |
+| `ui.text` | Command prompts, popup text, etc. |
+| `ui.text.focus` | |
+| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
+| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
+| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
+| `ui.virtual.whitespace` | Visible whitespace characters |
+| `ui.virtual.indent-guide` | Vertical indent width guides |
+| `ui.virtual.inlay-hint` | Default style for inlay hints of all kinds |
+| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
+| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
+| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
+| `ui.menu` | Code and command completion menus |
+| `ui.menu.selected` | Selected autocomplete item |
+| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
+| `ui.selection` | For selections in the editing area |
+| `ui.selection.primary` | |
+| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
+| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
+| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
+| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
+| `warning` | Diagnostics warning (gutter) |
+| `error` | Diagnostics error (gutter) |
+| `info` | Diagnostics info (gutter) |
+| `hint` | Diagnostics hint (gutter) |
+| `diagnostic` | Diagnostics fallback style (editing area) |
+| `diagnostic.hint` | Diagnostics hint (editing area) |
+| `diagnostic.info` | Diagnostics info (editing area) |
+| `diagnostic.warning` | Diagnostics warning (editing area) |
+| `diagnostic.error` | Diagnostics error (editing area) |
[editor-section]: ./configuration.md#editor-section
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index 6b5da17e..58ddb038 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -35,7 +35,7 @@ pub enum DiagnosticTag {
Deprecated,
}
-/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
+/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.94.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,
diff --git a/helix-core/src/doc_formatter/test.rs b/helix-core/src/doc_formatter/test.rs
index e68b31fd..ac8918bb 100644
--- a/helix-core/src/doc_formatter/test.rs
+++ b/helix-core/src/doc_formatter/test.rs
@@ -119,16 +119,7 @@ fn overlay() {
"foobar",
0,
false,
- &[
- Overlay {
- char_idx: 0,
- grapheme: "X".into(),
- },
- Overlay {
- char_idx: 2,
- grapheme: "\t".into(),
- },
- ]
+ &[Overlay::new(0, "X"), Overlay::new(2, "\t")],
),
"Xo bar "
);
@@ -138,18 +129,9 @@ fn overlay() {
0,
true,
&[
- Overlay {
- char_idx: 2,
- grapheme: "\t".into(),
- },
- Overlay {
- char_idx: 5,
- grapheme: "\t".into(),
- },
- Overlay {
- char_idx: 16,
- grapheme: "X".into(),
- },
+ Overlay::new(2, "\t"),
+ Overlay::new(5, "\t"),
+ Overlay::new(16, "X"),
]
),
"fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
@@ -170,24 +152,14 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
#[test]
fn annotation() {
assert_eq!(
- annotate_text(
- "bar",
- false,
- &[InlineAnnotation {
- char_idx: 0,
- text: "foo".into(),
- }]
- ),
+ annotate_text("bar", false, &[InlineAnnotation::new(0, "foo")]),
"foobar "
);
assert_eq!(
annotate_text(
&"foo ".repeat(10),
true,
- &[InlineAnnotation {
- char_idx: 0,
- text: "foo ".into(),
- }]
+ &[InlineAnnotation::new(0, "foo ")]
),
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
);
@@ -199,20 +171,8 @@ fn annotation_and_overlay() {
"bbar".into(),
&TextFormat::new_test(false),
TextAnnotations::default()
- .add_inline_annotations(
- Rc::new([InlineAnnotation {
- char_idx: 0,
- text: "fooo".into(),
- }]),
- None
- )
- .add_overlay(
- Rc::new([Overlay {
- char_idx: 0,
- grapheme: "\t".into(),
- }]),
- None
- ),
+ .add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
+ .add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
0,
)
.0
diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs
index 1956f6b5..3e48de4d 100644
--- a/helix-core/src/text_annotations.rs
+++ b/helix-core/src/text_annotations.rs
@@ -15,6 +15,15 @@ pub struct InlineAnnotation {
pub char_idx: usize,
}
+impl InlineAnnotation {
+ pub fn new(char_idx: usize, text: impl Into<Tendril>) -> Self {
+ Self {
+ char_idx,
+ text: text.into(),
+ }
+ }
+}
+
/// Represents a **single Grapheme** that is part of the document
/// that start at `char_idx` that will be replaced with
/// a different `grapheme`.
@@ -33,22 +42,13 @@ pub struct InlineAnnotation {
/// use helix_core::text_annotations::Overlay;
///
/// // replaces a
-/// Overlay {
-/// char_idx: 0,
-/// grapheme: "X".into(),
-/// };
+/// Overlay::new(0, "X");
///
/// // replaces X͎̊͢͜͝͡
-/// Overlay{
-/// char_idx: 1,
-/// grapheme: "\t".into(),
-/// };
+/// Overlay::new(1, "\t");
///
/// // replaces b
-/// Overlay{
-/// char_idx: 6,
-/// grapheme: "X̢̢̟͖̲͌̋̇͑͝".into(),
-/// };
+/// Overlay::new(6, "X̢̢̟͖̲͌̋̇͑͝");
/// ```
///
/// The following examples are invalid uses
@@ -57,16 +57,10 @@ pub struct InlineAnnotation {
/// use helix_core::text_annotations::Overlay;
///
/// // overlay is not aligned at grapheme boundary
-/// Overlay{
-/// char_idx: 3,
-/// grapheme: "x".into(),
-/// };
+/// Overlay::new(3, "x");
///
/// // overlay contains multiple graphemes
-/// Overlay{
-/// char_idx: 0,
-/// grapheme: "xy".into(),
-/// };
+/// Overlay::new(0, "xy");
/// ```
#[derive(Debug, Clone)]
pub struct Overlay {
@@ -74,6 +68,15 @@ pub struct Overlay {
pub grapheme: Tendril,
}
+impl Overlay {
+ pub fn new(char_idx: usize, grapheme: impl Into<Tendril>) -> Self {
+ Self {
+ char_idx,
+ grapheme: grapheme.into(),
+ }
+ }
+}
+
/// Line annotations allow for virtual text between normal
/// text lines. They cause `height` empty lines to be inserted
/// below the document line that contains `anchor_char_idx`.
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 9fa118fb..9cb7c147 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -315,6 +315,9 @@ impl Client {
execute_command: Some(lsp::DynamicRegistrationClientCapabilities {
dynamic_registration: Some(false),
}),
+ inlay_hint: Some(lsp::InlayHintWorkspaceClientCapabilities {
+ refresh_support: Some(false),
+ }),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -386,6 +389,10 @@ impl Client {
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
..Default::default()
}),
+ inlay_hint: Some(lsp::InlayHintClientCapabilities {
+ dynamic_registration: Some(false),
+ resolve_support: None,
+ }),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
@@ -726,6 +733,31 @@ impl Client {
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}
+ pub fn text_document_range_inlay_hints(
+ &self,
+ text_document: lsp::TextDocumentIdentifier,
+ range: lsp::Range,
+ work_done_token: Option<lsp::ProgressToken>,
+ ) -> Option<impl Future<Output = Result<Value>>> {
+ let capabilities = self.capabilities.get().unwrap();
+
+ match capabilities.inlay_hint_provider {
+ Some(
+ lsp::OneOf::Left(true)
+ | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
+ ) => (),
+ _ => return None,
+ }
+
+ let params = lsp::InlayHintParams {
+ text_document,
+ range,
+ work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
+ };
+
+ Some(self.call::<lsp::request::InlayHintRequest>(params))
+ }
+
pub fn text_document_hover(
&self,
text_document: lsp::TextDocumentIdentifier,
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,
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(
diff --git a/languages.toml b/languages.toml
index 86f4a64d..83a09b0b 100644
--- a/languages.toml
+++ b/languages.toml
@@ -19,6 +19,14 @@ indent = { tab-width = 4, unit = " " }
'"' = '"'
'`' = '`'
+[language.config]
+inlayHints.bindingModeHints.enable = false
+inlayHints.closingBraceHints.minLines = 10
+inlayHints.closureReturnTypeHints.enable = "with_block"
+inlayHints.discriminantHints.enable = "fieldless"
+inlayHints.lifetimeElisionHints.enable = "skip_trivial"
+inlayHints.typeHints.hideClosureInitialization = false
+
[language.debugger]
name = "lldb-vscode"
transport = "stdio"
@@ -291,6 +299,14 @@ language-server = { command = "gopls" }
# TODO: gopls needs utf-8 offsets?
indent = { tab-width = 4, unit = "\t" }
+[language.config.hints]
+assignVariableTypes = true
+compositeLiteralFields = true
+constantValues = true
+functionTypeParameters = true
+parameterNames = true
+rangeVariableTypes = true
+
[language.debugger]
name = "go"
transport = "tcp"
@@ -382,6 +398,18 @@ comment-token = "//"
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
indent = { tab-width = 2, unit = " " }
+[language.config]
+hostInfo = "helix"
+
+[language.config.javascript.inlayHints]
+includeInlayEnumMemberValueHints = true
+includeInlayFunctionLikeReturnTypeHints = true
+includeInlayFunctionParameterTypeHints = true
+includeInlayParameterNameHints = "all"
+includeInlayParameterNameHintsWhenArgumentMatchesName = true
+includeInlayPropertyDeclarationTypeHints = true
+includeInlayVariableTypeHints = true
+
[language.debugger]
name = "node-debug2"
transport = "stdio"
@@ -409,6 +437,18 @@ language-server = { command = "typescript-language-server", args = ["--stdio"],
indent = { tab-width = 2, unit = " " }
grammar = "javascript"
+[language.config]
+hostInfo = "helix"
+
+[language.config.javascript.inlayHints]
+includeInlayEnumMemberValueHints = true
+includeInlayFunctionLikeReturnTypeHints = true
+includeInlayFunctionParameterTypeHints = true
+includeInlayParameterNameHints = "all"
+includeInlayParameterNameHintsWhenArgumentMatchesName = true
+includeInlayPropertyDeclarationTypeHints = true
+includeInlayVariableTypeHints = true
+
[[language]]
name = "typescript"
scope = "source.ts"
@@ -420,6 +460,18 @@ roots = []
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
indent = { tab-width = 2, unit = " " }
+[language.config]
+hostInfo = "helix"
+
+[language.config.typescript.inlayHints]
+includeInlayEnumMemberValueHints = true
+includeInlayFunctionLikeReturnTypeHints = true
+includeInlayFunctionParameterTypeHints = true
+includeInlayParameterNameHints = "all"
+includeInlayParameterNameHintsWhenArgumentMatchesName = true
+includeInlayPropertyDeclarationTypeHints = true
+includeInlayVariableTypeHints = true
+
[[grammar]]
name = "typescript"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
@@ -434,6 +486,18 @@ roots = []
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
indent = { tab-width = 2, unit = " " }
+[language.config]
+hostInfo = "helix"
+
+[language.config.typescript.inlayHints]
+includeInlayEnumMemberValueHints = true
+includeInlayFunctionLikeReturnTypeHints = true
+includeInlayFunctionParameterTypeHints = true
+includeInlayParameterNameHints = "all"
+includeInlayParameterNameHintsWhenArgumentMatchesName = true
+includeInlayPropertyDeclarationTypeHints = true
+includeInlayVariableTypeHints = true
+
[[grammar]]
name = "tsx"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@@ -740,6 +804,14 @@ comment-token = "--"
indent = { tab-width = 2, unit = " " }
language-server = { command = "lua-language-server", args = [] }
+[language.config.Lua.hint]
+enable = true
+arrayIndex = "Enable"
+setType = true
+paramName = "All"
+paramType = true
+await = true
+
[[grammar]]
name = "lua"
source = { git = "https://github.com/MunifTanjim/tree-sitter-lua", rev = "887dfd4e83c469300c279314ff1619b1d0b85b91" }