Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--helix-term/src/commands.rs128
1 files changed, 100 insertions, 28 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 8024165c..c2dd13af 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1340,15 +1340,16 @@ fn selection_overlaps_document_link(
}
}
-/// Resolve a document link target, using the LSP resolve request when needed.
-fn resolve_document_link_target(
+/// Create a document link resolve request when the target isn't already present.
+///
+/// This only builds the LSP request. The request is awaited from a background
+/// job so `goto_file_impl` does not block the UI thread while the language
+/// server resolves the target.
+fn resolve_document_link_request(
editor: &Editor,
link: &helix_view::document::DocumentLink,
-) -> Option<Url> {
- if let Some(target) = link.link.target.clone() {
- return Some(target);
- }
-
+) -> Option<impl Future<Output = helix_lsp::Result<helix_lsp::lsp::DocumentLink>> + Send + 'static>
+{
let language_server = editor.language_server_by_id(link.language_server_id)?;
let supports_resolve = language_server
.capabilities()
@@ -1361,8 +1362,7 @@ fn resolve_document_link_target(
return None;
}
- let future = language_server.resolve_document_link(link.link.clone())?;
- helix_lsp::block_on(future).ok()?.target
+ language_server.resolve_document_link(link.link.clone())
}
/// Goto files/URLs in selection.
@@ -1381,6 +1381,8 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
let mut lsp_targets = Vec::new();
let mut lsp_targets_seen = HashSet::new();
+ let mut unresolved_links = HashSet::new();
+ let mut resolve_requests = Vec::new();
let mut fallback_ranges = Vec::new();
if doc.document_links.is_empty() {
@@ -1393,10 +1395,14 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
continue;
}
matched = true;
- if let Some(target) = resolve_document_link_target(cx.editor, link) {
+ if let Some(target) = link.link.target.clone() {
if lsp_targets_seen.insert(target.clone()) {
lsp_targets.push(target);
}
+ } else if unresolved_links.insert((link.start, link.end, link.language_server_id)) {
+ if let Some(request) = resolve_document_link_request(cx.editor, link) {
+ resolve_requests.push(request);
+ }
}
}
if !matched {
@@ -1409,6 +1415,37 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
open_url(cx, target, action);
}
+ if !resolve_requests.is_empty() {
+ let rel_path = rel_path.clone();
+ cx.jobs.callback(async move {
+ let mut targets = Vec::new();
+ let mut seen = HashSet::new();
+
+ // Resolve links off the main thread, then hand the resulting URLs
+ // back to the editor/compositor callback once all requests finish.
+ for request in resolve_requests {
+ match request.await {
+ Ok(link) => {
+ if let Some(target) = link.target {
+ if seen.insert(target.clone()) {
+ targets.push(target);
+ }
+ }
+ }
+ Err(err) => log::warn!("Failed to resolve document link: {err}"),
+ }
+ }
+
+ Ok(Callback::EditorCompositor(Box::new(
+ move |editor, compositor| {
+ for target in targets {
+ open_url_in_callback(editor, compositor, target, action, &rel_path);
+ }
+ },
+ )))
+ });
+ }
+
if fallback_ranges.is_empty() {
return;
}
@@ -1462,7 +1499,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
}
/// Opens the given url. If the URL points to a valid textual file it is open in helix.
-// Otherwise, the file is open using external program.
+/// Otherwise, the file is open using external program.
fn open_url(cx: &mut Context, url: Url, action: Action) {
let doc = doc!(cx.editor);
let rel_path = doc
@@ -1470,10 +1507,60 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
.map(|path| path.parent().unwrap().to_path_buf())
.unwrap_or_default();
- if url.scheme() != "file" {
+ if should_open_url_externally(&url) {
return cx.jobs.callback(crate::open_external_url_callback(url));
}
+ let path = &rel_path.join(url.path());
+ if path.is_dir() {
+ let picker = ui::file_picker(cx.editor, path.into());
+ cx.push_layer(Box::new(overlaid(picker)));
+ } else if let Err(e) = cx.editor.open(path, action) {
+ cx.editor.set_error(format!("Open file failed: {:?}", e));
+ }
+}
+
+/// Open a URL from an editor/compositor callback.
+///
+/// This mirrors `open_url` but does not require a full `Context`, which makes
+/// it usable from async job completions such as deferred document link
+/// resolves.
+fn open_url_in_callback(
+ editor: &mut Editor,
+ compositor: &mut Compositor,
+ url: Url,
+ action: Action,
+ rel_path: &Path,
+) {
+ if should_open_url_externally(&url) {
+ tokio::spawn(async move {
+ match crate::open_external_url_callback(url).await {
+ Ok(callback) => job::dispatch_callback(callback).await,
+ Err(err) => status::report(err).await,
+ }
+ });
+ return;
+ }
+
+ let path = &rel_path.join(url.path());
+ if path.is_dir() {
+ let picker = ui::file_picker(editor, path.into());
+ compositor.push(Box::new(overlaid(picker)));
+ } else if let Err(e) = editor.open(path, action) {
+ editor.set_error(format!("Open file failed: {:?}", e));
+ }
+}
+
+/// Returns whether a URL should opened externally.
+///
+/// Non-`file` URLs always open externally. `file` URLs are opened externally
+/// only when the target looks like a binary file (a non-textual file that can't
+/// be viewed in helix).
+fn should_open_url_externally(url: &Url) -> bool {
+ if url.scheme() != "file" {
+ return true;
+ }
+
let content_type = std::fs::File::open(url.path()).and_then(|file| {
// Read up to 1kb to detect the content type
let mut read_buffer = Vec::new();
@@ -1481,22 +1568,7 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
Ok(content_inspector::inspect(&read_buffer[..n]))
});
- // we attempt to open binary files - files that can't be open in helix - using external
- // program as well, e.g. pdf files or images
- match content_type {
- Ok(content_inspector::ContentType::BINARY) => {
- cx.jobs.callback(crate::open_external_url_callback(url))
- }
- Ok(_) | Err(_) => {
- let path = &rel_path.join(url.path());
- if path.is_dir() {
- let picker = ui::file_picker(cx.editor, path.into());
- cx.push_layer(Box::new(overlaid(picker)));
- } else if let Err(e) = cx.editor.open(path, action) {
- cx.editor.set_error(format!("Open file failed: {:?}", e));
- }
- }
- }
+ matches!(content_type, Ok(content_inspector::ContentType::BINARY))
}
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)