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
pull/5/head
Poliorcetics 2 years ago committed by GitHub
parent 3d230e701d
commit bdcd4d9411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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

@ -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,

@ -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

@ -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`.

@ -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,

@ -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

@ -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)
}

@ -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,

@ -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,

@ -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)]

@ -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);

@ -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,
}
}

@ -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(

@ -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" }

Loading…
Cancel
Save