@@ -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,