aboutsummaryrefslogtreecommitdiff
path: root/src/app/actions.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-06 23:59:59 +0300
committerGitHub <noreply@github.com>2026-07-06 23:59:59 +0300
commit4f22e9a73ba4d9029468e078235c5f5ce629d179 (patch)
tree49b56a29305192c8270aa43e87bb5f1d86d00233 /src/app/actions.rs
parent56edcbcba83b8ff7f6f69ee4231f163e97df3346 (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/actions.rs')
-rw-r--r--src/app/actions.rs136
1 files changed, 129 insertions, 7 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 019b6692..52ebbbb7 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -1999,15 +1999,23 @@ impl AppState {
}
let metrics = self.pane_scroll_metrics(terminal_runtimes, pane_id);
- let row_selection = Selection::range(
+ let visible_selection = Selection::line_range(
pane_id,
- viewport_row,
- 0,
+ Selection::absolute_row_for_viewport(0, metrics),
+ Selection::absolute_row_for_viewport(info.inner_rect.height.saturating_sub(1), metrics),
info.inner_rect.width.saturating_sub(1),
- metrics,
);
- let row_text = rt.extract_selection(&row_selection)?;
- url_at_column(&row_text, col).map(str::to_owned)
+ let visible_text = rt.extract_selection(&visible_selection)?;
+ let logical_cell =
+ logical_cell_for_visible_cell(&visible_text, info.inner_rect.width, viewport_row, col)?;
+ let line_start = visible_text[..logical_cell.byte_index]
+ .rfind('\n')
+ .map_or(0, |idx| idx + 1);
+ let line_end = visible_text[logical_cell.byte_index..]
+ .find('\n')
+ .map_or(visible_text.len(), |idx| logical_cell.byte_index + idx);
+ let line = visible_text.get(line_start..line_end)?;
+ url_at_column(line, logical_cell.logical_col).map(str::to_owned)
}
pub fn copy_selection(&mut self, terminal_runtimes: &crate::terminal::TerminalRuntimeRegistry) {
@@ -2088,12 +2096,126 @@ fn word_bounds_at_column(row: &str, col: u16) -> Option<(u16, u16)> {
pub(crate) fn url_at_column(row: &str, col: u16) -> Option<&str> {
let cells = text_cells(row);
let clicked_idx = cell_index_at_column(&cells, col)?;
- let span = url_span_at_column(&cells, clicked_idx)?;
+ let span = url_spans(&cells)
+ .into_iter()
+ .find(|span| span.contains(clicked_idx))?;
let start_byte = byte_index_for_cell(row, span.start);
let end_byte = byte_index_after_cell(row, span.end);
safe_web_url(row.get(start_byte..end_byte)?)
}
+pub(crate) fn visible_url_spans(row: &str) -> Vec<(u16, u16, &str)> {
+ let cells = text_cells(row);
+ url_spans(&cells)
+ .into_iter()
+ .filter_map(|span| {
+ let start_byte = byte_index_for_cell(row, span.start);
+ let end_byte = byte_index_after_cell(row, span.end);
+ let url = row.get(start_byte..end_byte).and_then(safe_web_url)?;
+ let (start_col, end_col) = span.columns(&cells);
+ Some((start_col, end_col, url))
+ })
+ .collect()
+}
+
+fn url_spans(cells: &[TextCell]) -> Vec<CellSpan> {
+ let mut spans = Vec::new();
+ let mut start = 0;
+ while start < cells.len() {
+ if starts_with_chars(&cells[start..], "http://")
+ || starts_with_chars(&cells[start..], "https://")
+ {
+ let mut end = start;
+ while end + 1 < cells.len() && !cells[end + 1].ch.is_whitespace() {
+ end += 1;
+ }
+ if let Some(span) = trim_url_edges(cells, CellSpan { start, end }) {
+ spans.push(span);
+ }
+ start = end + 1;
+ } else {
+ start += 1;
+ }
+ }
+ spans
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct VisibleTextCell {
+ pub(crate) byte_index: usize,
+ pub(crate) ch: char,
+ pub(crate) logical_col: u16,
+ pub(crate) screen_row: u16,
+ pub(crate) screen_col: u16,
+}
+
+pub(crate) fn visible_text_cells(text: &str, pane_width: u16) -> Vec<VisibleTextCell> {
+ if pane_width == 0 {
+ return Vec::new();
+ }
+
+ let mut cells = Vec::new();
+ let mut screen_row = 0u16;
+ let mut screen_col = 0u16;
+ let mut logical_col = 0u16;
+ let mut pending_wrap = false;
+ for (byte_index, ch) in text.char_indices() {
+ if ch == '\n' {
+ screen_row = screen_row.saturating_add(1);
+ screen_col = 0;
+ logical_col = 0;
+ pending_wrap = false;
+ continue;
+ }
+ if pending_wrap {
+ screen_row = screen_row.saturating_add(1);
+ screen_col = 0;
+ pending_wrap = false;
+ }
+
+ let width = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
+ cells.push(VisibleTextCell {
+ byte_index,
+ ch,
+ logical_col,
+ screen_row,
+ screen_col,
+ });
+
+ logical_col = logical_col.saturating_add(width);
+ screen_col = screen_col.saturating_add(width);
+ while screen_col > pane_width {
+ screen_col -= pane_width;
+ screen_row = screen_row.saturating_add(1);
+ }
+ if width > 0 && screen_col == pane_width {
+ pending_wrap = true;
+ screen_col = pane_width.saturating_sub(1);
+ }
+ }
+ cells
+}
+
+pub(crate) fn logical_cell_for_visible_cell(
+ text: &str,
+ pane_width: u16,
+ target_row: u16,
+ target_col: u16,
+) -> Option<VisibleTextCell> {
+ visible_text_cells(text, pane_width)
+ .into_iter()
+ .find(|cell| {
+ let width = UnicodeWidthChar::width(cell.ch).unwrap_or(0) as u16;
+ cell.screen_row == target_row
+ && if width == 0 {
+ target_col == cell.screen_col
+ } else {
+ target_col >= cell.screen_col
+ && target_col < cell.screen_col.saturating_add(width)
+ }
+ })
+}
+
fn token_span_at_column(cells: &[TextCell], clicked_idx: usize) -> Option<CellSpan> {
if is_word_separator(cells[clicked_idx].ch) {
return None;