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/actions.rs | |
| 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/actions.rs')
| -rw-r--r-- | src/app/actions.rs | 136 |
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; |
