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
pull/7190/head
Matouš Dzivjak 1 year ago committed by GitHub
parent bfd60a5b39
commit 3052050ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

38
Cargo.lock generated

@ -1237,6 +1237,7 @@ dependencies = [
"log", "log",
"nucleo", "nucleo",
"once_cell", "once_cell",
"open",
"pulldown-cmark", "pulldown-cmark",
"serde", "serde",
"serde_json", "serde_json",
@ -1247,6 +1248,7 @@ dependencies = [
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"toml", "toml",
"url",
"which", "which",
] ]
@ -1410,6 +1412,25 @@ version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.6" version = "1.0.6"
@ -1610,6 +1631,17 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 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]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@ -1639,6 +1671,12 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.0" version = "2.3.0"

@ -57,6 +57,10 @@ pulldown-cmark = { version = "0.9", default-features = false }
# file type detection # file type detection
content_inspector = "0.2.4" content_inspector = "0.2.4"
# opening URLs
open = "5.0.0"
url = "2.4.1"
# config # config
toml = "0.7" toml = "0.7"

@ -60,8 +60,13 @@ use crate::{
use crate::job::{self, Jobs}; use crate::job::{self, Jobs};
use futures_util::{stream::FuturesUnordered, TryStreamExt}; use futures_util::{stream::FuturesUnordered, TryStreamExt};
use std::{collections::HashMap, fmt, future::Future}; use std::{
use std::{collections::HashSet, num::NonZeroUsize}; collections::{HashMap, HashSet},
fmt,
future::Future,
io::Read,
num::NonZeroUsize,
};
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -70,6 +75,7 @@ use std::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use url::Url;
use grep_regex::RegexMatcherBuilder; use grep_regex::RegexMatcherBuilder;
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
@ -331,7 +337,7 @@ impl MappableCommand {
goto_implementation, "Goto implementation", goto_implementation, "Goto implementation",
goto_file_start, "Goto line number <n> else file start", goto_file_start, "Goto line number <n> else file start",
goto_file_end, "Goto file end", 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_hsplit, "Goto files in selection (hsplit)",
goto_file_vsplit, "Goto files in selection (vsplit)", goto_file_vsplit, "Goto files in selection (vsplit)",
goto_reference, "Goto references", goto_reference, "Goto references",
@ -1190,10 +1196,53 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
.to_string(), .to_string(),
); );
} }
for sel in paths { for sel in paths {
let p = sel.trim(); let p = sel.trim();
if !p.is_empty() { if p.is_empty() {
let path = &rel_path.join(p); 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() { if path.is_dir() {
let picker = ui::file_picker(path.into(), &cx.editor.config()); let picker = ui::file_picker(path.into(), &cx.editor.config());
cx.push_layer(Box::new(overlaid(picker))); 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<F>(cx: &mut Context, extend_fn: F) fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
where where
F: Fn(RopeSlice, Range, usize) -> Range, F: Fn(RopeSlice, Range, usize) -> Range,

@ -26,7 +26,7 @@ helix-vcs = { version = "0.6", path = "../helix-vcs" }
# Conversion traits # Conversion traits
once_cell = "1.18" once_cell = "1.18"
url = "2" url = "2.4.1"
arc-swap = { version = "1.6.0" } arc-swap = { version = "1.6.0" }

Loading…
Cancel
Save