Skip to content

Commit 347fcc3

Browse files
committed
feat(agent): make conversation links navigable
1 parent 937b721 commit 347fcc3

5 files changed

Lines changed: 732 additions & 38 deletions

File tree

src/editor.rs

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
323349
fn 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

Comments
 (0)