on master help sign in
open urls with goto_file command (#5820)

* feat(commands): open urls with goto_file command Add capability for `goto_file` command to open an URL under cursor. Fixes: https://github.com/helix-editor/helix/issues/1472 Superseds: https://github.com/helix-editor/helix/pull/4398 * open files inside helix * address code review * bump deps * fix based on code review comments

commit3052050ee0388207048318fed0909e63a2c865f9 authorMatouš Dzivjak <matousdzivjak@gmail.com> dateTue 21 Nov 2023, 12:04 (3 years ago) parentbfd60a5
4 files changed+114−6
Cargo.lock+38 helix-term/Cargo.toml+4 helix-term/src/commands.rs+71−5 helix-view/Cargo.toml+1−1
Cargo.lock+38
@@ -1237,6 +1237,7 @@ dependencies = [
12371237 "log",
12381238 "nucleo",
12391239 "once_cell",
1240+ "open",
12401241 "pulldown-cmark",
12411242 "serde",
12421243 "serde_json",
@@ -1247,6 +1248,7 @@ dependencies = [
12471248 "tokio",
12481249 "tokio-stream",
12491250 "toml",
1251+ "url",
12501252 "which",
12511253]
12521254
@@ -1410,6 +1412,25 @@ version = "2.0.4"
14101412source = "registry+https://github.com/rust-lang/crates.io-index"
14111413checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
14121414
1415+[[package]]
1416+name = "is-docker"
1417+version = "0.2.0"
1418+source = "registry+https://github.com/rust-lang/crates.io-index"
1419+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
1420+dependencies = [
1421+ "once_cell",
1422+]
1423+
1424+[[package]]
1425+name = "is-wsl"
1426+version = "0.4.0"
1427+source = "registry+https://github.com/rust-lang/crates.io-index"
1428+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
1429+dependencies = [
1430+ "is-docker",
1431+ "once_cell",
1432+]
1433+
14131434[[package]]
14141435name = "itoa"
14151436version = "1.0.6"
@@ -1610,6 +1631,17 @@ version = "1.18.0"
16101631source = "registry+https://github.com/rust-lang/crates.io-index"
16111632checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
16121633
1634+[[package]]
1635+name = "open"
1636+version = "5.0.0"
1637+source = "registry+https://github.com/rust-lang/crates.io-index"
1638+checksum = "cfabf1927dce4d6fdf563d63328a0a506101ced3ec780ca2135747336c98cef8"
1639+dependencies = [
1640+ "is-wsl",
1641+ "libc",
1642+ "pathdiff",
1643+]
1644+
16131645[[package]]
16141646name = "option-ext"
16151647version = "0.2.0"
@@ -1639,6 +1671,12 @@ dependencies = [
16391671 "windows-sys 0.45.0",
16401672]
16411673
1674+[[package]]
1675+name = "pathdiff"
1676+version = "0.2.1"
1677+source = "registry+https://github.com/rust-lang/crates.io-index"
1678+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
1679+
16421680[[package]]
16431681name = "percent-encoding"
16441682version = "2.3.0"
helix-term/Cargo.toml+4
@@ -57,6 +57,10 @@ pulldown-cmark = { version = "0.9", default-features = false }
5757# file type detection
5858content_inspector = "0.2.4"
5959
60+# opening URLs
61+open = "5.0.0"
62+url = "2.4.1"
63+
6064# config
6165toml = "0.7"
6266
helix-term/src/commands.rs+71−5
@@ -60,8 +60,13 @@ use crate::{
6060
6161use crate::job::{self, Jobs};
6262use futures_util::{stream::FuturesUnordered, TryStreamExt};
63−use std::{collections::HashMap, fmt, future::Future};
64−use std::{collections::HashSet, num::NonZeroUsize};
63+use std::{
64+ collections::{HashMap, HashSet},
65+ fmt,
66+ future::Future,
67+ io::Read,
68+ num::NonZeroUsize,
69+};
6570
6671use std::{
6772 borrow::Cow,
@@ -70,6 +75,7 @@ use std::{
7075
7176use once_cell::sync::Lazy;
7277use serde::de::{self, Deserialize, Deserializer};
78+use url::Url;
7379
7480use grep_regex::RegexMatcherBuilder;
7581use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
@@ -331,7 +337,7 @@ impl MappableCommand {
331337 goto_implementation, "Goto implementation",
332338 goto_file_start, "Goto line number <n> else file start",
333339 goto_file_end, "Goto file end",
334− goto_file, "Goto files in selection",
340+ goto_file, "Goto files/URLs in selection",
335341 goto_file_hsplit, "Goto files in selection (hsplit)",
336342 goto_file_vsplit, "Goto files in selection (vsplit)",
337343 goto_reference, "Goto references",
@@ -1190,10 +1196,53 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11901196 .to_string(),
11911197 );
11921198 }
1199+
11931200 for sel in paths {
11941201 let p = sel.trim();
1195− if !p.is_empty() {
1196− let path = &rel_path.join(p);
1202+ if p.is_empty() {
1203+ continue;
1204+ }
1205+
1206+ if let Ok(url) = Url::parse(p) {
1207+ return open_url(cx, url, action);
1208+ }
1209+
1210+ let path = &rel_path.join(p);
1211+ if path.is_dir() {
1212+ let picker = ui::file_picker(path.into(), &cx.editor.config());
1213+ cx.push_layer(Box::new(overlaid(picker)));
1214+ } else if let Err(e) = cx.editor.open(path, action) {
1215+ cx.editor.set_error(format!("Open file failed: {:?}", e));
1216+ }
1217+ }
1218+}
1219+
1220+/// Opens the given url. If the URL points to a valid textual file it is open in helix.
1221+// Otherwise, the file is open using external program.
1222+fn open_url(cx: &mut Context, url: Url, action: Action) {
1223+ let doc = doc!(cx.editor);
1224+ let rel_path = doc
1225+ .relative_path()
1226+ .map(|path| path.parent().unwrap().to_path_buf())
1227+ .unwrap_or_default();
1228+
1229+ if url.scheme() != "file" {
1230+ return open_external_url(cx, url);
1231+ }
1232+
1233+ let content_type = std::fs::File::open(url.path()).and_then(|file| {
1234+ // Read up to 1kb to detect the content type
1235+ let mut read_buffer = Vec::new();
1236+ let n = file.take(1024).read_to_end(&mut read_buffer)?;
1237+ Ok(content_inspector::inspect(&read_buffer[..n]))
1238+ });
1239+
1240+ // we attempt to open binary files - files that can't be open in helix - using external
1241+ // program as well, e.g. pdf files or images
1242+ match content_type {
1243+ Ok(content_inspector::ContentType::BINARY) => open_external_url(cx, url),
1244+ Ok(_) | Err(_) => {
1245+ let path = &rel_path.join(url.path());
11971246 if path.is_dir() {
11981247 let picker = ui::file_picker(path.into(), &cx.editor.config());
11991248 cx.push_layer(Box::new(overlaid(picker)));
@@ -1204,6 +1253,23 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
12041253 }
12051254}
12061255
1256+/// Opens URL in external program.
1257+fn open_external_url(cx: &mut Context, url: Url) {
1258+ let commands = open::commands(url.as_str());
1259+ cx.jobs.callback(async {
1260+ for cmd in commands {
1261+ let mut command = tokio::process::Command::new(cmd.get_program());
1262+ command.args(cmd.get_args());
1263+ if command.output().await.is_ok() {
1264+ return Ok(job::Callback::Editor(Box::new(|_| {})));
1265+ }
1266+ }
1267+ Ok(job::Callback::Editor(Box::new(move |editor| {
1268+ editor.set_error("Opening URL in external program failed")
1269+ })))
1270+ });
1271+}
1272+
12071273fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
12081274where
12091275 F: Fn(RopeSlice, Range, usize) -> Range,
helix-view/Cargo.toml+1−1
@@ -26,7 +26,7 @@ helix-vcs = { version = "0.6", path = "../helix-vcs" }
2626
2727# Conversion traits
2828once_cell = "1.18"
29−url = "2"
29+url = "2.4.1"
3030
3131arc-swap = { version = "1.6.0" }
3232