@@ -320,6 +320,32 @@ fn expanded_path_string(path: &str) -> anyhow::Result<String> {
320320 Ok(expand_user_path(path)?.to_string_lossy().into_owned())
321321}
322322
323+ fn open_external_url(url: &str) -> std::io::Result<tokio::process::Child> {
324+ #[cfg(target_os = "macos")]
325+ {
326+ return tokio::process::Command::new("open")
327+ .arg("--")
328+ .arg(url)
329+ .spawn();
330+ }
331+ #[cfg(all(unix, not(target_os = "macos")))]
332+ {
333+ return tokio::process::Command::new("xdg-open").arg(url).spawn();
334+ }
335+ #[cfg(windows)]
336+ {
337+ return tokio::process::Command::new("rundll32.exe")
338+ .arg("url.dll,FileProtocolHandler")
339+ .arg(url)
340+ .spawn();
341+ }
342+ #[allow(unreachable_code)]
343+ Err(std::io::Error::new(
344+ std::io::ErrorKind::Unsupported,
345+ "opening links is not supported on this platform",
346+ ))
347+ }
348+
323349fn plugin_lsp_error(message: &str) -> Value {
324350 json!({
325351 "ok": false,
@@ -1012,6 +1038,7 @@ pub enum Action {
10121038 MoveTo(usize, usize),
10131039 MoveToFilePos(String, usize, usize),
10141040 OpenLocation(plugin::PluginLocation, plugin::OpenLocationTarget),
1041+ OpenExternalUrl(String),
10151042 MoveToNextWord,
10161043 MoveToPreviousWord,
10171044 MoveToNextBigWord,
@@ -8862,6 +8889,25 @@ impl Editor {
88628889 return Some(KeyAction::Single(Action::Refresh));
88638890 }
88648891 }
8892+ if matches!(event.code, KeyCode::Tab | KeyCode::BackTab) {
8893+ let panel_height = usize::from(self.size.1.saturating_sub(2));
8894+ let forward = event.code == KeyCode::Tab;
8895+ if self.panel_manager.select_focused_text_link(
8896+ forward,
8897+ panel_height,
8898+ usize::from(self.size.0),
8899+ ) {
8900+ return Some(KeyAction::Single(Action::Refresh));
8901+ }
8902+ }
8903+ if event.code == KeyCode::Enter {
8904+ if let Some(target) = self
8905+ .panel_manager
8906+ .focused_text_link_target(usize::from(self.size.0))
8907+ {
8908+ return Some(self.follow_text_panel_link(target));
8909+ }
8910+ }
88658911 let action = match event.code {
88668912 KeyCode::Esc => {
88678913 self.panel_manager.focus_editor();
@@ -8912,10 +8958,17 @@ impl Editor {
89128958 let height = self.size.1 as usize;
89138959
89148960 match event.kind {
8915- MouseEventKind::Down(MouseButton::Left) => self
8916- .panel_manager
8917- .focus_panel_at_position(x, y, width, height)
8918- .and_then(Self::panel_event_key_action),
8961+ MouseEventKind::Down(MouseButton::Left) => {
8962+ if let Some(target) = self
8963+ .panel_manager
8964+ .text_link_at_position(x, y, width, height)
8965+ {
8966+ return Some(self.follow_text_panel_link(target));
8967+ }
8968+ self.panel_manager
8969+ .focus_panel_at_position(x, y, width, height)
8970+ .and_then(Self::panel_event_key_action)
8971+ }
89198972 MouseEventKind::ScrollUp => {
89208973 let id = self
89218974 .panel_manager
@@ -8949,6 +9002,26 @@ impl Editor {
89499002 })
89509003 }
89519004
9005+ fn follow_text_panel_link(&mut self, target: plugin::TextPanelLinkTarget) -> KeyAction {
9006+ match target {
9007+ plugin::TextPanelLinkTarget::File { path, line, column } => {
9008+ self.panel_manager.focus_editor();
9009+ KeyAction::Single(Action::OpenLocation(
9010+ plugin::PluginLocation {
9011+ path,
9012+ line: line.saturating_sub(1),
9013+ column: column.saturating_sub(1),
9014+ column_encoding: plugin::LocationColumnEncoding::Utf8Byte,
9015+ },
9016+ plugin::OpenLocationTarget::Current,
9017+ ))
9018+ }
9019+ plugin::TextPanelLinkTarget::ExternalUrl(url) => {
9020+ KeyAction::Single(Action::OpenExternalUrl(url))
9021+ }
9022+ }
9023+ }
9024+
89529025 fn panel_global_key_action(&self, ev: &event::Event) -> Option<KeyAction> {
89539026 let key = Self::key_string_for_event(ev)?;
89549027 let action = self
@@ -13138,6 +13211,24 @@ impl Editor {
1313813211 self.execute_with_tracking(&Action::MoveTo(*x, *y), buffer, runtime, false)
1313913212 .await?;
1314013213 }
13214+ Action::OpenExternalUrl(url) => {
13215+ let lowercase = url.to_ascii_lowercase();
13216+ if !lowercase.starts_with("https://") && !lowercase.starts_with("http://") {
13217+ self.last_error = Some("Only HTTP and HTTPS links can be opened".to_string());
13218+ return Ok(false);
13219+ }
13220+ match open_external_url(url) {
13221+ Ok(mut child) => {
13222+ tokio::spawn(async move {
13223+ let _ = child.wait().await;
13224+ });
13225+ }
13226+ Err(error) => {
13227+ self.last_error = Some(format!("Unable to open link: {error}"));
13228+ return Ok(false);
13229+ }
13230+ }
13231+ }
1314113232 Action::OpenLocation(location, target) => {
1314213233 let path = match expanded_path_string(&location.path).and_then(|path| {
1314313234 Ok(Path::new(&path)
0 commit comments