diff options
| author | akbash <akbash@herdr.dev> | 2026-07-06 23:59:59 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-06 23:59:59 +0300 |
| commit | 4f22e9a73ba4d9029468e078235c5f5ce629d179 (patch) | |
| tree | 49b56a29305192c8270aa43e87bb5f1d86d00233 /src/app/input | |
| parent | 56edcbcba83b8ff7f6f69ee4231f163e97df3346 (diff) | |
fix: preserve wrapped pane links (#1099)
refs #1098
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app/input')
| -rw-r--r-- | src/app/input/terminal.rs | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index 282fa263..c9abbbc2 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -561,6 +561,150 @@ mod tests { } #[tokio::test] + async fn pane_cell_url_resolver_finds_soft_wrapped_url() { + let (_app, info) = app_with_screen_bytes(b""); + let prefix = "https://example.com/"; + let padding = "a".repeat(info.inner_rect.width as usize - prefix.len()); + let url = format!("{prefix}{padding}tail"); + let (app, _info) = app_with_screen_bytes(url.as_bytes()); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + + assert_eq!( + app.state + .url_at_pane_cell(&app.terminal_runtimes, pane_id, 1, 1) + .as_deref(), + Some(url.as_str()) + ); + } + + #[tokio::test] + async fn pane_cell_url_resolver_does_not_shift_after_zero_width_mark() { + let url = "https://example.com/mark"; + let screen = format!("e\u{301} {url}"); + let (app, _info) = app_with_screen_bytes(screen.as_bytes()); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + + assert_eq!( + app.state + .url_at_pane_cell(&app.terminal_runtimes, pane_id, 0, 2) + .as_deref(), + Some(url) + ); + } + + #[tokio::test] + async fn pane_cell_url_resolver_handles_hard_newline_after_full_row() { + let (_app, info) = app_with_screen_bytes(b""); + let full_row = "x".repeat(info.inner_rect.width as usize); + let url = "https://example.com/next"; + let screen = format!("{full_row}\n{url}"); + let (app, _info) = app_with_screen_bytes(screen.as_bytes()); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + + assert_eq!( + app.state + .url_at_pane_cell(&app.terminal_runtimes, pane_id, 1, 1) + .as_deref(), + None + ); + assert_eq!( + app.state + .url_at_pane_cell(&app.terminal_runtimes, pane_id, 2, 1) + .as_deref(), + Some(url) + ); + } + + #[tokio::test] + async fn render_stream_synthesizes_soft_wrapped_url_hyperlinks() { + let (_app, info) = app_with_screen_bytes(b""); + let prefix = "https://example.com/"; + let padding = "b".repeat(info.inner_rect.width as usize - prefix.len()); + let url = format!("{prefix}{padding}tail"); + let (app, info) = app_with_screen_bytes(url.as_bytes()); + + let links = crate::server::render_stream::visible_hyperlinks( + &app.state, + &app.terminal_runtimes, + None, + ); + + assert!(links.iter().any(|((x, y), symbol, uri)| { + *x == info.inner_rect.x + 1 + && *y == info.inner_rect.y + 1 + && symbol == "a" + && uri == &url + })); + } + + #[tokio::test] + async fn render_stream_does_not_shift_url_hyperlinks_after_zero_width_mark() { + let url = "https://example.com/mark"; + let screen = format!("e\u{301} {url}"); + let (app, info) = app_with_screen_bytes(screen.as_bytes()); + + let links = crate::server::render_stream::visible_hyperlinks( + &app.state, + &app.terminal_runtimes, + None, + ); + + assert!(links.iter().any(|((x, y), symbol, uri)| { + *x == info.inner_rect.x + 2 && *y == info.inner_rect.y && symbol == "h" && uri == url + })); + } + + #[tokio::test] + async fn render_stream_handles_hard_newline_after_full_row() { + let (_app, info) = app_with_screen_bytes(b""); + let full_row = "x".repeat(info.inner_rect.width as usize); + let url = "https://example.com/next"; + let screen = format!("{full_row}\n{url}"); + let (app, info) = app_with_screen_bytes(screen.as_bytes()); + let links = crate::server::render_stream::visible_hyperlinks( + &app.state, + &app.terminal_runtimes, + None, + ); + + assert!(links.iter().any(|((x, y), symbol, uri)| { + *x == info.inner_rect.x + 1 + && *y == info.inner_rect.y + 2 + && symbol == "t" + && uri == url + })); + } + + #[tokio::test] + async fn render_stream_filters_plain_url_when_final_cell_differs() { + let line = "see https://example.com/hidden"; + let (mut app, _info) = app_with_screen_bytes(line.as_bytes()); + let (mut buffer, _cursor) = + crate::server::render_stream::render_virtual_with_runtime_registry( + &mut app.state, + &app.terminal_runtimes, + ratatui::layout::Rect::new(0, 0, 106, 20), + false, + crate::kitty_graphics::HostCellSize::default(), + ); + let info = app.state.view.pane_infos[0].clone(); + let target_x = info.inner_rect.x + line.find("https").expect("url start") as u16; + let target_y = info.inner_rect.y; + buffer[(target_x, target_y)] + .set_style(ratatui::style::Style::default().fg(ratatui::style::Color::Red)); + + let links = crate::server::render_stream::visible_hyperlinks( + &app.state, + &app.terminal_runtimes, + Some(&buffer), + ); + + assert!(!links + .iter() + .any(|((x, y), _, uri)| *x == target_x && *y == target_y && uri == &line[4..])); + } + + #[tokio::test] async fn pane_cell_url_resolver_prefers_osc8_hyperlink() { let (app, _info) = app_with_screen_bytes( b"\x1b]8;;https://example.com/hidden-target\x1b\\label\x1b]8;;\x1b\\", |
