mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 02:46:17 +04:00
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
This commit is contained in:
parent
3d230e701d
commit
bdcd4d9411
@ -120,9 +120,12 @@ ### `[editor.lsp]` Section
|
|||||||
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
|
| `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` |
|
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
|
||||||
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
|
| `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` |
|
| `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.
|
[^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
|
### `[editor.cursor-shape]` Section
|
||||||
|
|
||||||
|
@ -262,58 +262,61 @@ #### Interface
|
|||||||
- `hover` - for hover popup UI
|
- `hover` - for hover popup UI
|
||||||
|
|
||||||
|
|
||||||
| Key | Notes |
|
| Key | Notes |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ui.background` | |
|
| `ui.background` | |
|
||||||
| `ui.background.separator` | Picker separator below input line |
|
| `ui.background.separator` | Picker separator below input line |
|
||||||
| `ui.cursor` | |
|
| `ui.cursor` | |
|
||||||
| `ui.cursor.normal` | |
|
| `ui.cursor.normal` | |
|
||||||
| `ui.cursor.insert` | |
|
| `ui.cursor.insert` | |
|
||||||
| `ui.cursor.select` | |
|
| `ui.cursor.select` | |
|
||||||
| `ui.cursor.match` | Matching bracket etc. |
|
| `ui.cursor.match` | Matching bracket etc. |
|
||||||
| `ui.cursor.primary` | Cursor with primary selection |
|
| `ui.cursor.primary` | Cursor with primary selection |
|
||||||
| `ui.cursor.primary.normal` | |
|
| `ui.cursor.primary.normal` | |
|
||||||
| `ui.cursor.primary.insert` | |
|
| `ui.cursor.primary.insert` | |
|
||||||
| `ui.cursor.primary.select` | |
|
| `ui.cursor.primary.select` | |
|
||||||
| `ui.gutter` | Gutter |
|
| `ui.gutter` | Gutter |
|
||||||
| `ui.gutter.selected` | Gutter for the line the cursor is on |
|
| `ui.gutter.selected` | Gutter for the line the cursor is on |
|
||||||
| `ui.linenr` | Line numbers |
|
| `ui.linenr` | Line numbers |
|
||||||
| `ui.linenr.selected` | Line number for the line the cursor is on |
|
| `ui.linenr.selected` | Line number for the line the cursor is on |
|
||||||
| `ui.statusline` | Statusline |
|
| `ui.statusline` | Statusline |
|
||||||
| `ui.statusline.inactive` | Statusline (unfocused document) |
|
| `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.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.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.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
|
||||||
| `ui.statusline.separator` | Separator character in statusline |
|
| `ui.statusline.separator` | Separator character in statusline |
|
||||||
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
||||||
| `ui.popup.info` | Prompt for multiple key options |
|
| `ui.popup.info` | Prompt for multiple key options |
|
||||||
| `ui.window` | Borderlines separating splits |
|
| `ui.window` | Borderlines separating splits |
|
||||||
| `ui.help` | Description box for commands |
|
| `ui.help` | Description box for commands |
|
||||||
| `ui.text` | Command prompts, popup text, etc. |
|
| `ui.text` | Command prompts, popup text, etc. |
|
||||||
| `ui.text.focus` | |
|
| `ui.text.focus` | |
|
||||||
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
|
| `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.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.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
|
||||||
| `ui.virtual.whitespace` | Visible whitespace characters |
|
| `ui.virtual.whitespace` | Visible whitespace characters |
|
||||||
| `ui.virtual.indent-guide` | Vertical indent width guides |
|
| `ui.virtual.indent-guide` | Vertical indent width guides |
|
||||||
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
|
| `ui.virtual.inlay-hint` | Default style for inlay hints of all kinds |
|
||||||
| `ui.menu` | Code and command completion menus |
|
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
|
||||||
| `ui.menu.selected` | Selected autocomplete item |
|
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
|
||||||
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
|
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
|
||||||
| `ui.selection` | For selections in the editing area |
|
| `ui.menu` | Code and command completion menus |
|
||||||
| `ui.selection.primary` | |
|
| `ui.menu.selected` | Selected autocomplete item |
|
||||||
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
|
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
|
||||||
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
|
| `ui.selection` | For selections in the editing area |
|
||||||
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
|
| `ui.selection.primary` | |
|
||||||
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
|
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
|
||||||
| `warning` | Diagnostics warning (gutter) |
|
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
|
||||||
| `error` | Diagnostics error (gutter) |
|
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
|
||||||
| `info` | Diagnostics info (gutter) |
|
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
|
||||||
| `hint` | Diagnostics hint (gutter) |
|
| `warning` | Diagnostics warning (gutter) |
|
||||||
| `diagnostic` | Diagnostics fallback style (editing area) |
|
| `error` | Diagnostics error (gutter) |
|
||||||
| `diagnostic.hint` | Diagnostics hint (editing area) |
|
| `info` | Diagnostics info (gutter) |
|
||||||
| `diagnostic.info` | Diagnostics info (editing area) |
|
| `hint` | Diagnostics hint (gutter) |
|
||||||
| `diagnostic.warning` | Diagnostics warning (editing area) |
|
| `diagnostic` | Diagnostics fallback style (editing area) |
|
||||||
| `diagnostic.error` | Diagnostics error (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
|
[editor-section]: ./configuration.md#editor-section
|
||||||
|
@ -35,7 +35,7 @@ pub enum DiagnosticTag {
|
|||||||
Deprecated,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
pub range: Range,
|
pub range: Range,
|
||||||
|
@ -119,16 +119,7 @@ fn overlay() {
|
|||||||
"foobar",
|
"foobar",
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
&[
|
&[Overlay::new(0, "X"), Overlay::new(2, "\t")],
|
||||||
Overlay {
|
|
||||||
char_idx: 0,
|
|
||||||
grapheme: "X".into(),
|
|
||||||
},
|
|
||||||
Overlay {
|
|
||||||
char_idx: 2,
|
|
||||||
grapheme: "\t".into(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
"Xo bar "
|
"Xo bar "
|
||||||
);
|
);
|
||||||
@ -138,18 +129,9 @@ fn overlay() {
|
|||||||
0,
|
0,
|
||||||
true,
|
true,
|
||||||
&[
|
&[
|
||||||
Overlay {
|
Overlay::new(2, "\t"),
|
||||||
char_idx: 2,
|
Overlay::new(5, "\t"),
|
||||||
grapheme: "\t".into(),
|
Overlay::new(16, "X"),
|
||||||
},
|
|
||||||
Overlay {
|
|
||||||
char_idx: 5,
|
|
||||||
grapheme: "\t".into(),
|
|
||||||
},
|
|
||||||
Overlay {
|
|
||||||
char_idx: 16,
|
|
||||||
grapheme: "X".into(),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
|
"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]
|
#[test]
|
||||||
fn annotation() {
|
fn annotation() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
annotate_text(
|
annotate_text("bar", false, &[InlineAnnotation::new(0, "foo")]),
|
||||||
"bar",
|
|
||||||
false,
|
|
||||||
&[InlineAnnotation {
|
|
||||||
char_idx: 0,
|
|
||||||
text: "foo".into(),
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
"foobar "
|
"foobar "
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
annotate_text(
|
annotate_text(
|
||||||
&"foo ".repeat(10),
|
&"foo ".repeat(10),
|
||||||
true,
|
true,
|
||||||
&[InlineAnnotation {
|
&[InlineAnnotation::new(0, "foo ")]
|
||||||
char_idx: 0,
|
|
||||||
text: "foo ".into(),
|
|
||||||
}]
|
|
||||||
),
|
),
|
||||||
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
|
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
|
||||||
);
|
);
|
||||||
@ -199,20 +171,8 @@ fn annotation_and_overlay() {
|
|||||||
"bbar".into(),
|
"bbar".into(),
|
||||||
&TextFormat::new_test(false),
|
&TextFormat::new_test(false),
|
||||||
TextAnnotations::default()
|
TextAnnotations::default()
|
||||||
.add_inline_annotations(
|
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
|
||||||
Rc::new([InlineAnnotation {
|
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
|
||||||
char_idx: 0,
|
|
||||||
text: "fooo".into(),
|
|
||||||
}]),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
.add_overlay(
|
|
||||||
Rc::new([Overlay {
|
|
||||||
char_idx: 0,
|
|
||||||
grapheme: "\t".into(),
|
|
||||||
}]),
|
|
||||||
None
|
|
||||||
),
|
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
|
@ -15,6 +15,15 @@ pub struct InlineAnnotation {
|
|||||||
pub char_idx: usize,
|
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
|
/// Represents a **single Grapheme** that is part of the document
|
||||||
/// that start at `char_idx` that will be replaced with
|
/// that start at `char_idx` that will be replaced with
|
||||||
/// a different `grapheme`.
|
/// a different `grapheme`.
|
||||||
@ -33,22 +42,13 @@ pub struct InlineAnnotation {
|
|||||||
/// use helix_core::text_annotations::Overlay;
|
/// use helix_core::text_annotations::Overlay;
|
||||||
///
|
///
|
||||||
/// // replaces a
|
/// // replaces a
|
||||||
/// Overlay {
|
/// Overlay::new(0, "X");
|
||||||
/// char_idx: 0,
|
|
||||||
/// grapheme: "X".into(),
|
|
||||||
/// };
|
|
||||||
///
|
///
|
||||||
/// // replaces X͎̊͢͜͝͡
|
/// // replaces X͎̊͢͜͝͡
|
||||||
/// Overlay{
|
/// Overlay::new(1, "\t");
|
||||||
/// char_idx: 1,
|
|
||||||
/// grapheme: "\t".into(),
|
|
||||||
/// };
|
|
||||||
///
|
///
|
||||||
/// // replaces b
|
/// // replaces b
|
||||||
/// Overlay{
|
/// Overlay::new(6, "X̢̢̟͖̲͌̋̇͑͝");
|
||||||
/// char_idx: 6,
|
|
||||||
/// grapheme: "X̢̢̟͖̲͌̋̇͑͝".into(),
|
|
||||||
/// };
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// The following examples are invalid uses
|
/// The following examples are invalid uses
|
||||||
@ -57,16 +57,10 @@ pub struct InlineAnnotation {
|
|||||||
/// use helix_core::text_annotations::Overlay;
|
/// use helix_core::text_annotations::Overlay;
|
||||||
///
|
///
|
||||||
/// // overlay is not aligned at grapheme boundary
|
/// // overlay is not aligned at grapheme boundary
|
||||||
/// Overlay{
|
/// Overlay::new(3, "x");
|
||||||
/// char_idx: 3,
|
|
||||||
/// grapheme: "x".into(),
|
|
||||||
/// };
|
|
||||||
///
|
///
|
||||||
/// // overlay contains multiple graphemes
|
/// // overlay contains multiple graphemes
|
||||||
/// Overlay{
|
/// Overlay::new(0, "xy");
|
||||||
/// char_idx: 0,
|
|
||||||
/// grapheme: "xy".into(),
|
|
||||||
/// };
|
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Overlay {
|
pub struct Overlay {
|
||||||
@ -74,6 +68,15 @@ pub struct Overlay {
|
|||||||
pub grapheme: Tendril,
|
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
|
/// Line annotations allow for virtual text between normal
|
||||||
/// text lines. They cause `height` empty lines to be inserted
|
/// text lines. They cause `height` empty lines to be inserted
|
||||||
/// below the document line that contains `anchor_char_idx`.
|
/// below the document line that contains `anchor_char_idx`.
|
||||||
|
@ -315,6 +315,9 @@ pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
|
|||||||
execute_command: Some(lsp::DynamicRegistrationClientCapabilities {
|
execute_command: Some(lsp::DynamicRegistrationClientCapabilities {
|
||||||
dynamic_registration: Some(false),
|
dynamic_registration: Some(false),
|
||||||
}),
|
}),
|
||||||
|
inlay_hint: Some(lsp::InlayHintWorkspaceClientCapabilities {
|
||||||
|
refresh_support: Some(false),
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
text_document: Some(lsp::TextDocumentClientCapabilities {
|
text_document: Some(lsp::TextDocumentClientCapabilities {
|
||||||
@ -386,6 +389,10 @@ pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
|
|||||||
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
|
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
inlay_hint: Some(lsp::InlayHintClientCapabilities {
|
||||||
|
dynamic_registration: Some(false),
|
||||||
|
resolve_support: None,
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
window: Some(lsp::WindowClientCapabilities {
|
window: Some(lsp::WindowClientCapabilities {
|
||||||
@ -726,6 +733,31 @@ pub fn text_document_signature_help(
|
|||||||
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
|
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(
|
pub fn text_document_hover(
|
||||||
&self,
|
&self,
|
||||||
text_document: lsp::TextDocumentIdentifier,
|
text_document: lsp::TextDocumentIdentifier,
|
||||||
|
@ -114,17 +114,7 @@ pub fn callback<T, F>(
|
|||||||
T: for<'de> serde::Deserialize<'de> + Send + 'static,
|
T: for<'de> serde::Deserialize<'de> + Send + 'static,
|
||||||
F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
|
F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
|
||||||
{
|
{
|
||||||
let callback = Box::pin(async move {
|
self.jobs.callback(make_job_callback(call, callback));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns 1 if no explicit count was provided
|
/// Returns 1 if no explicit count was provided
|
||||||
@ -134,6 +124,27 @@ pub fn count(&self) -> usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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};
|
use helix_view::{align_view, Align};
|
||||||
|
|
||||||
/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
|
/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
|
||||||
|
@ -15,8 +15,13 @@
|
|||||||
|
|
||||||
use super::{align_view, push_jump, Align, Context, Editor, Open};
|
use super::{align_view, push_jump, Align, Context, Editor, Open};
|
||||||
|
|
||||||
use helix_core::{path, Selection};
|
use helix_core::{path, text_annotations::InlineAnnotation, Selection};
|
||||||
use helix_view::{document::Mode, editor::Action, theme::Style};
|
use helix_view::{
|
||||||
|
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
|
||||||
|
editor::Action,
|
||||||
|
theme::Style,
|
||||||
|
Document, View,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
compositor::{self, Compositor},
|
compositor::{self, Compositor},
|
||||||
@ -27,7 +32,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
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
|
/// 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 @@ pub fn clear_completion(&mut self, editor: &mut Editor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
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 {
|
if let Some(completion) = &mut self.completion {
|
||||||
return if completion.ensure_item_resolved(cx) {
|
return if completion.ensure_item_resolved(cx) {
|
||||||
EventResult::Consumed(None)
|
EventResult::Consumed(None)
|
||||||
@ -1014,6 +1016,10 @@ fn handle_mouse_event(
|
|||||||
event: &MouseEvent,
|
event: &MouseEvent,
|
||||||
cxt: &mut commands::Context,
|
cxt: &mut commands::Context,
|
||||||
) -> EventResult {
|
) -> EventResult {
|
||||||
|
if event.kind != MouseEventKind::Moved {
|
||||||
|
cxt.editor.reset_idle_timer();
|
||||||
|
}
|
||||||
|
|
||||||
let config = cxt.editor.config();
|
let config = cxt.editor.config();
|
||||||
let MouseEvent {
|
let MouseEvent {
|
||||||
kind,
|
kind,
|
||||||
|
@ -225,6 +225,9 @@ fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
|
|||||||
let loader = cx.editor.syn_loader.clone();
|
let loader = cx.editor.syn_loader.clone();
|
||||||
doc.detect_language(loader);
|
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)
|
EventResult::Consumed(None)
|
||||||
@ -339,6 +342,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
inner,
|
inner,
|
||||||
doc,
|
doc,
|
||||||
offset,
|
offset,
|
||||||
|
// TODO: compute text annotations asynchronously here (like inlay hints)
|
||||||
&TextAnnotations::default(),
|
&TextAnnotations::default(),
|
||||||
highlights,
|
highlights,
|
||||||
&cx.editor.theme,
|
&cx.editor.theme,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
use helix_core::auto_pairs::AutoPairs;
|
use helix_core::auto_pairs::AutoPairs;
|
||||||
use helix_core::doc_formatter::TextFormat;
|
use helix_core::doc_formatter::TextFormat;
|
||||||
use helix_core::syntax::Highlight;
|
use helix_core::syntax::Highlight;
|
||||||
use helix_core::text_annotations::TextAnnotations;
|
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
|
||||||
use helix_core::Range;
|
use helix_core::Range;
|
||||||
use helix_vcs::{DiffHandle, DiffProviderRegistry};
|
use helix_vcs::{DiffHandle, DiffProviderRegistry};
|
||||||
|
|
||||||
@ -19,6 +19,7 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::rc::Rc;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
@ -119,6 +120,14 @@ pub struct Document {
|
|||||||
text: Rope,
|
text: Rope,
|
||||||
selections: HashMap<ViewId, Selection>,
|
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>,
|
path: Option<PathBuf>,
|
||||||
encoding: &'static encoding::Encoding,
|
encoding: &'static encoding::Encoding,
|
||||||
|
|
||||||
@ -162,6 +171,73 @@ pub struct Document {
|
|||||||
version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
|
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};
|
use std::{fmt, mem};
|
||||||
impl fmt::Debug for Document {
|
impl fmt::Debug for Document {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
@ -169,6 +245,8 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||||||
.field("id", &self.id)
|
.field("id", &self.id)
|
||||||
.field("text", &self.text)
|
.field("text", &self.text)
|
||||||
.field("selections", &self.selections)
|
.field("selections", &self.selections)
|
||||||
|
.field("inlay_hints_oudated", &self.inlay_hints_oudated)
|
||||||
|
.field("text_annotations", &self.inlay_hints)
|
||||||
.field("path", &self.path)
|
.field("path", &self.path)
|
||||||
.field("encoding", &self.encoding)
|
.field("encoding", &self.encoding)
|
||||||
.field("restore_cursor", &self.restore_cursor)
|
.field("restore_cursor", &self.restore_cursor)
|
||||||
@ -187,6 +265,15 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// The documentation and implementation of this function should be up-to-date with
|
||||||
// its sibling function, `to_writer()`.
|
// its sibling function, `to_writer()`.
|
||||||
//
|
//
|
||||||
@ -389,6 +476,8 @@ pub fn from(
|
|||||||
encoding,
|
encoding,
|
||||||
text,
|
text,
|
||||||
selections: HashMap::default(),
|
selections: HashMap::default(),
|
||||||
|
inlay_hints: HashMap::default(),
|
||||||
|
inlay_hints_oudated: false,
|
||||||
indent_style: DEFAULT_INDENT,
|
indent_style: DEFAULT_INDENT,
|
||||||
line_ending: DEFAULT_LINE_ENDING,
|
line_ending: DEFAULT_LINE_ENDING,
|
||||||
restore_cursor: false,
|
restore_cursor: false,
|
||||||
@ -819,13 +908,16 @@ pub fn ensure_view_init(&mut self, view_id: ViewId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) {
|
pub fn remove_view(&mut self, view_id: ViewId) {
|
||||||
self.selections.remove(&view_id);
|
self.selections.remove(&view_id);
|
||||||
|
self.inlay_hints.remove(&view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a [`Transaction`] to the [`Document`] to change its text.
|
/// Apply a [`Transaction`] to the [`Document`] to change its text.
|
||||||
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
||||||
|
use helix_core::Assoc;
|
||||||
|
|
||||||
let old_doc = self.text().clone();
|
let old_doc = self.text().clone();
|
||||||
|
|
||||||
let success = transaction.changes().apply(&mut self.text);
|
let success = transaction.changes().apply(&mut self.text);
|
||||||
@ -881,10 +973,10 @@ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let changes = transaction.changes();
|
||||||
|
|
||||||
// map state.diagnostics over changes::map_pos too
|
// map state.diagnostics over changes::map_pos too
|
||||||
for diagnostic in &mut self.diagnostics {
|
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.start = changes.map_pos(diagnostic.range.start, Assoc::After);
|
||||||
diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
|
diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
|
||||||
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
||||||
@ -892,13 +984,40 @@ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
|||||||
self.diagnostics
|
self.diagnostics
|
||||||
.sort_unstable_by_key(|diagnostic| diagnostic.range);
|
.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
|
// emit lsp notification
|
||||||
if let Some(language_server) = self.language_server() {
|
if let Some(language_server) = self.language_server() {
|
||||||
let notify = language_server.text_document_did_change(
|
let notify = language_server.text_document_did_change(
|
||||||
self.versioned_identifier(),
|
self.versioned_identifier(),
|
||||||
&old_doc,
|
&old_doc,
|
||||||
self.text(),
|
self.text(),
|
||||||
transaction.changes(),
|
changes,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(notify) = notify {
|
if let Some(notify) = notify {
|
||||||
@ -1217,6 +1336,7 @@ pub fn selection(&self, view_id: ViewId) -> &Selection {
|
|||||||
&self.selections[&view_id]
|
&self.selections[&view_id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn selections(&self) -> &HashMap<ViewId, Selection> {
|
pub fn selections(&self) -> &HashMap<ViewId, Selection> {
|
||||||
&self.selections
|
&self.selections
|
||||||
}
|
}
|
||||||
@ -1355,9 +1475,27 @@ pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> Tex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations {
|
||||||
TextAnnotations::default()
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -345,6 +345,8 @@ pub struct LspConfig {
|
|||||||
pub auto_signature_help: bool,
|
pub auto_signature_help: bool,
|
||||||
/// Display docs under signature help popup
|
/// Display docs under signature help popup
|
||||||
pub display_signature_help_docs: bool,
|
pub display_signature_help_docs: bool,
|
||||||
|
/// Display inlay hints
|
||||||
|
pub display_inlay_hints: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LspConfig {
|
impl Default for LspConfig {
|
||||||
@ -354,6 +356,7 @@ fn default() -> Self {
|
|||||||
display_messages: false,
|
display_messages: false,
|
||||||
auto_signature_help: true,
|
auto_signature_help: true,
|
||||||
display_signature_help_docs: true,
|
display_signature_help_docs: true,
|
||||||
|
display_inlay_hints: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1133,6 +1136,19 @@ fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
|
|||||||
|
|
||||||
fn _refresh(&mut self) {
|
fn _refresh(&mut self) {
|
||||||
let config = self.config();
|
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() {
|
for (view, _) in self.tree.views_mut() {
|
||||||
let doc = doc_mut!(self, &view.doc);
|
let doc = doc_mut!(self, &view.doc);
|
||||||
view.sync_changes(doc);
|
view.sync_changes(doc);
|
||||||
|
@ -278,16 +278,15 @@ pub fn get(&self, index: ViewId) -> &View {
|
|||||||
self.try_get(index).unwrap()
|
self.try_get(index).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to get reference to a [View] by index. Returns `None` if node content is not a [Content::View]
|
/// Try to get reference to a [View] by index. Returns `None` if node content is not a [`Content::View`].
|
||||||
/// # Panics
|
|
||||||
///
|
///
|
||||||
/// 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> {
|
pub fn try_get(&self, index: ViewId) -> Option<&View> {
|
||||||
match &self.nodes[index] {
|
match self.nodes.get(index) {
|
||||||
Node {
|
Some(Node {
|
||||||
content: Content::View(view),
|
content: Content::View(view),
|
||||||
..
|
..
|
||||||
} => Some(view),
|
}) => Some(view),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
align_view,
|
align_view,
|
||||||
|
document::DocumentInlayHints,
|
||||||
editor::{GutterConfig, GutterType},
|
editor::{GutterConfig, GutterType},
|
||||||
graphics::Rect,
|
graphics::Rect,
|
||||||
Align, Document, DocumentId, Theme, ViewId,
|
Align, Document, DocumentId, Theme, ViewId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
char_idx_at_visual_offset, doc_formatter::TextFormat, text_annotations::TextAnnotations,
|
char_idx_at_visual_offset, doc_formatter::TextFormat, syntax::Highlight,
|
||||||
visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection,
|
text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block,
|
||||||
Transaction,
|
Position, RopeSlice, Selection, Transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
fmt,
|
fmt,
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
const JUMP_LIST_CAPACITY: usize = 30;
|
const JUMP_LIST_CAPACITY: usize = 30;
|
||||||
@ -402,9 +404,50 @@ pub fn screen_coords_at_pos(
|
|||||||
Some(pos)
|
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 {
|
pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations {
|
||||||
// TODO custom annotations for custom views like side by side diffs
|
// 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(
|
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]
|
[language.debugger]
|
||||||
name = "lldb-vscode"
|
name = "lldb-vscode"
|
||||||
transport = "stdio"
|
transport = "stdio"
|
||||||
@ -291,6 +299,14 @@ language-server = { command = "gopls" }
|
|||||||
# TODO: gopls needs utf-8 offsets?
|
# TODO: gopls needs utf-8 offsets?
|
||||||
indent = { tab-width = 4, unit = "\t" }
|
indent = { tab-width = 4, unit = "\t" }
|
||||||
|
|
||||||
|
[language.config.hints]
|
||||||
|
assignVariableTypes = true
|
||||||
|
compositeLiteralFields = true
|
||||||
|
constantValues = true
|
||||||
|
functionTypeParameters = true
|
||||||
|
parameterNames = true
|
||||||
|
rangeVariableTypes = true
|
||||||
|
|
||||||
[language.debugger]
|
[language.debugger]
|
||||||
name = "go"
|
name = "go"
|
||||||
transport = "tcp"
|
transport = "tcp"
|
||||||
@ -382,6 +398,18 @@ comment-token = "//"
|
|||||||
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
|
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
|
||||||
indent = { tab-width = 2, unit = " " }
|
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]
|
[language.debugger]
|
||||||
name = "node-debug2"
|
name = "node-debug2"
|
||||||
transport = "stdio"
|
transport = "stdio"
|
||||||
@ -409,6 +437,18 @@ language-server = { command = "typescript-language-server", args = ["--stdio"],
|
|||||||
indent = { tab-width = 2, unit = " " }
|
indent = { tab-width = 2, unit = " " }
|
||||||
grammar = "javascript"
|
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]]
|
[[language]]
|
||||||
name = "typescript"
|
name = "typescript"
|
||||||
scope = "source.ts"
|
scope = "source.ts"
|
||||||
@ -420,6 +460,18 @@ roots = []
|
|||||||
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
|
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
|
||||||
indent = { tab-width = 2, unit = " " }
|
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]]
|
[[grammar]]
|
||||||
name = "typescript"
|
name = "typescript"
|
||||||
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "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" }
|
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
|
||||||
indent = { tab-width = 2, unit = " " }
|
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]]
|
[[grammar]]
|
||||||
name = "tsx"
|
name = "tsx"
|
||||||
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "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 = " " }
|
indent = { tab-width = 2, unit = " " }
|
||||||
language-server = { command = "lua-language-server", args = [] }
|
language-server = { command = "lua-language-server", args = [] }
|
||||||
|
|
||||||
|
[language.config.Lua.hint]
|
||||||
|
enable = true
|
||||||
|
arrayIndex = "Enable"
|
||||||
|
setType = true
|
||||||
|
paramName = "All"
|
||||||
|
paramType = true
|
||||||
|
await = true
|
||||||
|
|
||||||
[[grammar]]
|
[[grammar]]
|
||||||
name = "lua"
|
name = "lua"
|
||||||
source = { git = "https://github.com/MunifTanjim/tree-sitter-lua", rev = "887dfd4e83c469300c279314ff1619b1d0b85b91" }
|
source = { git = "https://github.com/MunifTanjim/tree-sitter-lua", rev = "887dfd4e83c469300c279314ff1619b1d0b85b91" }
|
||||||
|
Loading…
Reference in New Issue
Block a user