Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r--helix-term/src/commands.rs96
1 files changed, 86 insertions, 10 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index e36c694f..7abb5463 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1327,21 +1327,97 @@ fn goto_file_vsplit(cx: &mut Context) {
goto_file_impl(cx, Action::VerticalSplit);
}
-/// Goto files in selection.
+/// Returns true when a selection overlaps an LSP document link range.
+fn selection_overlaps_document_link(
+ selection: &Range,
+ link: &helix_view::document::DocumentLink,
+) -> bool {
+ if selection.is_empty() {
+ let pos = selection.from();
+ link.start <= pos && pos < link.end
+ } else {
+ selection.from() < link.end && selection.to() > link.start
+ }
+}
+
+/// Resolve a document link target, using the LSP resolve request when needed.
+fn resolve_document_link_target(
+ editor: &Editor,
+ link: &helix_view::document::DocumentLink,
+) -> Option<Url> {
+ if let Some(target) = link.link.target.clone() {
+ return Some(target);
+ }
+
+ let language_server = editor.language_server_by_id(link.language_server_id)?;
+ let supports_resolve = language_server
+ .capabilities()
+ .document_link_provider
+ .as_ref()?
+ .resolve_provider
+ .unwrap_or(false);
+
+ if !supports_resolve {
+ return None;
+ }
+
+ let future = language_server.resolve_document_link(link.link.clone())?;
+ helix_lsp::block_on(future).ok()?.target
+}
+
+/// Goto files/URLs in selection.
+///
+/// Prefers LSP document links when the cursor/selection overlaps a link range,
+/// falling back to the built-in path/URL detection otherwise.
fn goto_file_impl(cx: &mut Context, action: Action) {
let (view, doc) = current_ref!(cx.editor);
- let text = doc.text().slice(..);
- let selections = doc.selection(view.id);
- let primary = selections.primary();
+ let text = doc.text().clone();
+ let selections = doc.selection(view.id).ranges().to_vec();
let rel_path = doc
.relative_path()
.map(|path| path.parent().unwrap().to_path_buf())
.unwrap_or_default();
+ let text = text.slice(..);
+
+ let mut lsp_targets = Vec::new();
+ let mut lsp_targets_seen = HashSet::new();
+ let mut fallback_ranges = Vec::new();
- let paths: Vec<_> = if selections.len() == 1 && primary.len() == 1 {
+ if doc.document_links.is_empty() {
+ fallback_ranges.extend_from_slice(&selections);
+ } else {
+ for selection in &selections {
+ let mut matched = false;
+ for link in &doc.document_links {
+ if !selection_overlaps_document_link(selection, link) {
+ continue;
+ }
+ matched = true;
+ if let Some(target) = resolve_document_link_target(cx.editor, link) {
+ if lsp_targets_seen.insert(target.clone()) {
+ lsp_targets.push(target);
+ }
+ }
+ }
+ if !matched {
+ fallback_ranges.push(*selection);
+ }
+ }
+ }
+
+ for target in lsp_targets {
+ open_url(cx, target, action);
+ }
+
+ if fallback_ranges.is_empty() {
+ return;
+ }
+
+ let paths: Vec<_> = if fallback_ranges.len() == 1 && fallback_ranges[0].len() == 1 {
+ let selection = fallback_ranges[0];
// Cap the search at roughly 1k bytes around the cursor.
let lookaround = 1000;
- let pos = text.char_to_byte(primary.cursor(text));
+ let pos = text.char_to_byte(selection.cursor(text));
let search_start = text
.line_to_byte(text.byte_to_line(pos))
.max(text.floor_char_boundary(pos.saturating_sub(lookaround)));
@@ -1357,13 +1433,13 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
.find(|range| pos <= search_start + range.end)
.map(|range| Cow::from(search_range.byte_slice(range)));
log::debug!("goto_file auto-detected path: {path:?}");
- let path = path.unwrap_or_else(|| primary.fragment(text));
+ let path = path.unwrap_or_else(|| selection.fragment(text));
vec![path.into_owned()]
} else {
// Otherwise use each selection, trimmed.
- selections
- .fragments(text)
- .map(|sel| sel.trim().to_owned())
+ fallback_ranges
+ .iter()
+ .map(|range| range.fragment(text).trim().to_owned())
.filter(|sel| !sel.is_empty())
.collect()
};