diff --git a/Cargo.lock b/Cargo.lock index fda89964c..544a13bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1237,6 +1237,7 @@ dependencies = [ "log", "nucleo", "once_cell", + "open", "pulldown-cmark", "serde", "serde_json", @@ -1247,6 +1248,7 @@ dependencies = [ "tokio", "tokio-stream", "toml", + "url", "which", ] @@ -1410,6 +1412,25 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1610,6 +1631,17 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "open" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfabf1927dce4d6fdf563d63328a0a506101ced3ec780ca2135747336c98cef8" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -1639,6 +1671,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.3.0" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index c6374de54..378f25f87 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -57,6 +57,10 @@ pulldown-cmark = { version = "0.9", default-features = false } # file type detection content_inspector = "0.2.4" +# opening URLs +open = "5.0.0" +url = "2.4.1" + # config toml = "0.7" diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f1d1df063..bc54abaaf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -60,8 +60,13 @@ use crate::{ use crate::job::{self, Jobs}; use futures_util::{stream::FuturesUnordered, TryStreamExt}; -use std::{collections::HashMap, fmt, future::Future}; -use std::{collections::HashSet, num::NonZeroUsize}; +use std::{ + collections::{HashMap, HashSet}, + fmt, + future::Future, + io::Read, + num::NonZeroUsize, +}; use std::{ borrow::Cow, @@ -70,6 +75,7 @@ use std::{ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; +use url::Url; use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; @@ -331,7 +337,7 @@ impl MappableCommand { goto_implementation, "Goto implementation", goto_file_start, "Goto line number else file start", goto_file_end, "Goto file end", - goto_file, "Goto files in selection", + goto_file, "Goto files/URLs in selection", goto_file_hsplit, "Goto files in selection (hsplit)", goto_file_vsplit, "Goto files in selection (vsplit)", goto_reference, "Goto references", @@ -1190,10 +1196,53 @@ fn goto_file_impl(cx: &mut Context, action: Action) { .to_string(), ); } + for sel in paths { let p = sel.trim(); - if !p.is_empty() { - let path = &rel_path.join(p); + if p.is_empty() { + continue; + } + + if let Ok(url) = Url::parse(p) { + return open_url(cx, url, action); + } + + let path = &rel_path.join(p); + if path.is_dir() { + let picker = ui::file_picker(path.into(), &cx.editor.config()); + 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)); + } + } +} + +/// 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. +fn open_url(cx: &mut Context, url: Url, action: Action) { + let doc = doc!(cx.editor); + let rel_path = doc + .relative_path() + .map(|path| path.parent().unwrap().to_path_buf()) + .unwrap_or_default(); + + if url.scheme() != "file" { + return open_external_url(cx, url); + } + + 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(); + let n = file.take(1024).read_to_end(&mut read_buffer)?; + 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) => open_external_url(cx, url), + Ok(_) | Err(_) => { + let path = &rel_path.join(url.path()); if path.is_dir() { let picker = ui::file_picker(path.into(), &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); @@ -1204,6 +1253,23 @@ fn goto_file_impl(cx: &mut Context, action: Action) { } } +/// Opens URL in external program. +fn open_external_url(cx: &mut Context, url: Url) { + let commands = open::commands(url.as_str()); + cx.jobs.callback(async { + for cmd in commands { + let mut command = tokio::process::Command::new(cmd.get_program()); + command.args(cmd.get_args()); + if command.output().await.is_ok() { + return Ok(job::Callback::Editor(Box::new(|_| {}))); + } + } + Ok(job::Callback::Editor(Box::new(move |editor| { + editor.set_error("Opening URL in external program failed") + }))) + }); +} + fn extend_word_impl(cx: &mut Context, extend_fn: F) where F: Fn(RopeSlice, Range, usize) -> Range, diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 6218e9e41..a5218b9ac 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -26,7 +26,7 @@ helix-vcs = { version = "0.6", path = "../helix-vcs" } # Conversion traits once_cell = "1.18" -url = "2" +url = "2.4.1" arc-swap = { version = "1.6.0" }