Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--book/src/editor.md1
-rw-r--r--helix-term/src/handlers.rs2
-rw-r--r--helix-term/src/handlers/document_highlight.rs195
-rw-r--r--helix-term/src/ui/editor.rs26
-rw-r--r--helix-view/src/document.rs71
-rw-r--r--helix-view/src/editor.rs3
6 files changed, 298 insertions, 0 deletions
diff --git a/book/src/editor.md b/book/src/editor.md
index 0e65ed22..024f8642 100644
--- a/book/src/editor.md
+++ b/book/src/editor.md
@@ -166,6 +166,7 @@ The following statusline elements can be configured:
| `display-messages` | Display LSP `window/showMessage` messages below statusline[^1] | `true` |
| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
+| `auto-document-highlight` | Automatically highlight symbol references at the cursor | `false` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `inlay-hints-length-limit` | Maximum displayed length (non-zero number) of inlay hints | Unset by default |
| `display-color-swatches` | Show color swatches next to colors | `true` |
diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs
index 18297bfe..ff99e7fd 100644
--- a/helix-term/src/handlers.rs
+++ b/helix-term/src/handlers.rs
@@ -18,6 +18,7 @@ mod auto_save;
pub mod completion;
pub mod diagnostics;
mod document_colors;
+mod document_highlight;
mod prompt;
mod signature_help;
mod snippet;
@@ -46,6 +47,7 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
helix_view::handlers::register_hooks(&handlers);
completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers);
+ document_highlight::register_hooks(&handlers);
auto_save::register_hooks(&handlers);
diagnostics::register_hooks(&handlers);
snippet::register_hooks(&handlers);
diff --git a/helix-term/src/handlers/document_highlight.rs b/helix-term/src/handlers/document_highlight.rs
new file mode 100644
index 00000000..32403248
--- /dev/null
+++ b/helix-term/src/handlers/document_highlight.rs
@@ -0,0 +1,195 @@
+use helix_core::syntax::config::LanguageServerFeature;
+use helix_event::{cancelable_future, register_hook};
+use helix_lsp::{lsp, util::lsp_range_to_range, OffsetEncoding};
+use helix_view::{
+ events::{
+ ConfigDidChange, DocumentDidChange, DocumentDidOpen, LanguageServerExited,
+ LanguageServerInitialized, SelectionDidChange,
+ },
+ handlers::Handlers,
+ DocumentId, Editor, ViewId,
+};
+
+use crate::job;
+
+fn request_document_highlights(editor: &mut Editor, doc_id: DocumentId, view_id: ViewId) {
+ if !editor.config().lsp.auto_document_highlight {
+ return;
+ }
+
+ let Some(doc) = editor.document_mut(doc_id) else {
+ return;
+ };
+
+ doc.ensure_view_init(view_id);
+
+ let Some(language_server) = doc
+ .language_servers_with_feature(LanguageServerFeature::DocumentHighlight)
+ .next()
+ else {
+ doc.clear_document_highlights(view_id);
+ return;
+ };
+
+ let offset_encoding = language_server.offset_encoding();
+ let pos = doc.position(view_id, offset_encoding);
+ let Some(future) =
+ language_server.text_document_document_highlight(doc.identifier(), pos, None)
+ else {
+ doc.clear_document_highlights(view_id);
+ return;
+ };
+
+ let text = doc.text().clone();
+ let cancel = doc.document_highlight_controller(view_id).restart();
+
+ tokio::spawn(async move {
+ let response = match cancelable_future(future, &cancel).await {
+ Some(Ok(response)) => response,
+ Some(Err(err)) => {
+ log::error!("document highlight request failed: {err}");
+ return;
+ }
+ None => return,
+ };
+
+ let ranges = response
+ .map(|highlights| document_highlight_ranges(&text, offset_encoding, highlights))
+ .unwrap_or_default();
+
+ job::dispatch(move |editor, _| {
+ apply_document_highlights(editor, doc_id, view_id, ranges);
+ })
+ .await;
+ });
+}
+
+fn document_highlight_ranges(
+ text: &helix_core::Rope,
+ offset_encoding: OffsetEncoding,
+ highlights: Vec<lsp::DocumentHighlight>,
+) -> Vec<std::ops::Range<usize>> {
+ let slice = text.slice(..);
+ let mut ranges: Vec<_> = highlights
+ .into_iter()
+ .filter_map(|highlight| lsp_range_to_range(text, highlight.range, offset_encoding))
+ .map(|range| range.min_width_1(slice))
+ .filter_map(|range| {
+ let start = range.from();
+ let end = range.to();
+ (start < end).then_some(start..end)
+ })
+ .collect();
+
+ ranges.sort_by(|a, b| (a.start, a.end).cmp(&(b.start, b.end)));
+
+ let mut merged: Vec<std::ops::Range<usize>> = Vec::with_capacity(ranges.len());
+ for range in ranges {
+ if let Some(last) = merged.last_mut() {
+ if range.start <= last.end {
+ if range.end > last.end {
+ last.end = range.end;
+ }
+ continue;
+ }
+ }
+ merged.push(range);
+ }
+
+ merged
+}
+
+fn apply_document_highlights(
+ editor: &mut Editor,
+ doc_id: DocumentId,
+ view_id: ViewId,
+ ranges: Vec<std::ops::Range<usize>>,
+) {
+ if !editor.config().lsp.auto_document_highlight {
+ return;
+ }
+
+ let Some(doc) = editor.document_mut(doc_id) else {
+ return;
+ };
+
+ if !doc.has_language_server_with_feature(LanguageServerFeature::DocumentHighlight) {
+ doc.clear_document_highlights(view_id);
+ return;
+ }
+
+ if ranges.is_empty() {
+ doc.clear_document_highlights(view_id);
+ return;
+ }
+
+ doc.set_document_highlights(view_id, ranges);
+}
+
+pub(super) fn register_hooks(_handlers: &Handlers) {
+ register_hook!(move |event: &mut SelectionDidChange<'_>| {
+ if event.doc.config.load().lsp.auto_document_highlight {
+ let doc_id = event.doc.id();
+ let view_id = event.view;
+ job::dispatch_blocking(move |editor, _| {
+ request_document_highlights(editor, doc_id, view_id);
+ });
+ }
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut DocumentDidOpen<'_>| {
+ if !event.editor.config().lsp.auto_document_highlight {
+ return Ok(());
+ }
+ let view_id = event.editor.tree.focus;
+ if event.editor.tree.try_get(view_id).is_none() {
+ return Ok(());
+ }
+ request_document_highlights(event.editor, event.doc, view_id);
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut DocumentDidChange<'_>| {
+ if event.doc.config.load().lsp.auto_document_highlight && !event.ghost_transaction {
+ let doc_id = event.doc.id();
+ let view_id = event.view;
+ job::dispatch_blocking(move |editor, _| {
+ request_document_highlights(editor, doc_id, view_id);
+ });
+ }
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
+ if !event.editor.config().lsp.auto_document_highlight {
+ return Ok(());
+ }
+ let view_id = event.editor.tree.focus;
+ let Some(view) = event.editor.tree.try_get(view_id) else {
+ return Ok(());
+ };
+ let doc_id = view.doc;
+ request_document_highlights(event.editor, doc_id, view_id);
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut LanguageServerExited<'_>| {
+ for doc in event.editor.documents_mut() {
+ if doc.supports_language_server(event.server_id) {
+ doc.clear_all_document_highlights();
+ }
+ }
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut ConfigDidChange<'_>| {
+ if event.new.lsp.auto_document_highlight {
+ return Ok(());
+ }
+ for doc in event.editor.documents_mut() {
+ doc.clear_all_document_highlights();
+ }
+ Ok(())
+ });
+}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 9ffc3e87..c1d8205e 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -142,6 +142,11 @@ impl EditorView {
Self::doc_diagnostics_highlights_into(doc, theme, &mut overlays);
if is_focused {
+ if config.lsp.auto_document_highlight {
+ if let Some(overlay) = Self::doc_document_highlights(doc, view, theme) {
+ overlays.push(overlay);
+ }
+ }
if let Some(tabstops) = Self::tabstop_highlights(doc, theme) {
overlays.push(tabstops);
}
@@ -459,6 +464,27 @@ impl EditorView {
]);
}
+ pub fn doc_document_highlights(
+ doc: &Document,
+ view: &View,
+ theme: &Theme,
+ ) -> Option<OverlayHighlights> {
+ let ranges = doc.document_highlights(view.id)?;
+ if ranges.is_empty() {
+ return None;
+ }
+
+ let highlight = theme
+ .find_highlight_exact("ui.highlight")
+ .or_else(|| theme.find_highlight_exact("ui.selection"))
+ .or_else(|| theme.find_highlight_exact("ui.cursor"))?;
+
+ Some(OverlayHighlights::Homogeneous {
+ highlight,
+ ranges: ranges.to_vec(),
+ })
+ }
+
/// Get highlight spans for selections in a document view.
pub fn doc_selection_highlights(
mode: Mode,
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index c827274e..7a67ac7f 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -149,7 +149,10 @@ pub struct Document {
///
/// To know if they're up-to-date, check the `id` field in `DocumentInlayHints`.
pub(crate) inlay_hints: HashMap<ViewId, DocumentInlayHints>,
+ /// Jump label overlays for each view.
pub(crate) jump_labels: HashMap<ViewId, Vec<Overlay>>,
+ /// LSP document highlights for each view, stored as char ranges.
+ pub(crate) document_highlights: HashMap<ViewId, DocumentHighlights>,
/// 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,
@@ -211,6 +214,8 @@ pub struct Document {
// NOTE: ideally this would live on the handler for color swatches. This is blocked on a
// large refactor that would make `&mut Editor` available on the `DocumentDidChange` event.
pub color_swatch_controller: TaskController,
+ /// Per-view task controllers for canceling in-flight document highlight requests.
+ pub document_highlight_controllers: HashMap<ViewId, TaskController>,
pub pull_diagnostic_controller: TaskController,
// NOTE: this field should eventually go away - we should use the Editor's syn_loader instead
@@ -226,6 +231,12 @@ pub struct DocumentColorSwatches {
pub color_swatches_padding: Vec<InlineAnnotation>,
}
+/// Highlight ranges returned by LSP `textDocument/documentHighlight` for a view.
+#[derive(Debug, Clone, Default)]
+pub struct DocumentHighlights {
+ pub ranges: Vec<std::ops::Range<usize>>,
+}
+
/// 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
@@ -728,8 +739,10 @@ impl Document {
focused_at: std::time::Instant::now(),
readonly: false,
jump_labels: HashMap::new(),
+ document_highlights: HashMap::new(),
color_swatches: None,
color_swatch_controller: TaskController::new(),
+ document_highlight_controllers: HashMap::new(),
syn_loader,
previous_diagnostic_id: None,
pull_diagnostic_controller: TaskController::new(),
@@ -1380,6 +1393,8 @@ impl Document {
self.selections.remove(&view_id);
self.inlay_hints.remove(&view_id);
self.jump_labels.remove(&view_id);
+ self.document_highlights.remove(&view_id);
+ self.document_highlight_controllers.remove(&view_id);
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
@@ -1531,6 +1546,28 @@ impl Document {
apply_inlay_hint_changes(padding_after_inlay_hints);
}
+ for highlights in self.document_highlights.values_mut() {
+ let text_len = self.text.len_chars();
+ let mut updated = Vec::with_capacity(highlights.ranges.len());
+ for mut range in highlights.ranges.drain(..) {
+ changes.update_positions(
+ [
+ (&mut range.start, Assoc::After),
+ (&mut range.end, Assoc::After),
+ ]
+ .into_iter(),
+ );
+ if range.start >= text_len {
+ continue;
+ }
+ let end = range.end.min(text_len);
+ if range.start < end {
+ updated.push(range.start..end);
+ }
+ }
+ highlights.ranges = updated;
+ }
+
helix_event::dispatch(DocumentDidChange {
doc: self,
view: view_id,
@@ -2297,6 +2334,40 @@ impl Document {
self.jump_labels.remove(&view_id);
}
+ pub fn set_document_highlights(
+ &mut self,
+ view_id: ViewId,
+ ranges: Vec<std::ops::Range<usize>>,
+ ) {
+ if ranges.is_empty() {
+ self.document_highlights.remove(&view_id);
+ } else {
+ self.document_highlights
+ .insert(view_id, DocumentHighlights { ranges });
+ }
+ }
+
+ pub fn clear_document_highlights(&mut self, view_id: ViewId) {
+ self.document_highlights.remove(&view_id);
+ }
+
+ pub fn clear_all_document_highlights(&mut self) {
+ self.document_highlights.clear();
+ self.document_highlight_controllers.clear();
+ }
+
+ pub fn document_highlights(&self, view_id: ViewId) -> Option<&[std::ops::Range<usize>]> {
+ self.document_highlights
+ .get(&view_id)
+ .map(|highlights| highlights.ranges.as_slice())
+ }
+
+ pub fn document_highlight_controller(&mut self, view_id: ViewId) -> &mut TaskController {
+ self.document_highlight_controllers
+ .entry(view_id)
+ .or_default()
+ }
+
/// 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)
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index b7b4c442..f559938a 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -551,6 +551,8 @@ pub struct LspConfig {
pub display_signature_help_docs: bool,
/// Display inlay hints
pub display_inlay_hints: bool,
+ /// Automatically highlight symbol references at the cursor.
+ pub auto_document_highlight: bool,
/// Maximum displayed length of inlay hints (excluding the added trailing `…`).
/// If it's `None`, there's no limit
pub inlay_hints_length_limit: Option<NonZeroU8>,
@@ -571,6 +573,7 @@ impl Default for LspConfig {
auto_signature_help: true,
display_signature_help_docs: true,
display_inlay_hints: false,
+ auto_document_highlight: false,
inlay_hints_length_limit: None,
snippets: true,
goto_reference_include_declaration: true,