Enable plain clicks on transcript links and style bare URLs (#46884)

## Why

Transcript links required a modified click to open, and bare URLs lacked the visual styling of explicit Markdown links.

## What changed

- Open links on release after an unmodified, stationary left click. Cancel activation after dragging, scrolling, or releasing elsewhere so text selection remains usable.
- Apply Markdown link styling to bare URLs while preserving surrounding prose, existing emphasis, Unicode, and table wrapping.

## Testing

Add gesture tests for bare and Markdown links at narrow and wide widths, drag and scroll cancellation, and existing modified-click behavior. Add styling tests and snapshots covering punctuation, emphasis, tabs, Unicode, and wrapped table URLs.

GitOrigin-RevId: 23d12c901932d7e3586d841e5a0ece00e422fccd
This commit is contained in:
Eric Traut
2026-09-20 20:47:14 +00:00
committed by copyberry
parent 2f34d236f5
commit 9fdff739ea
8 changed files with 338 additions and 4 deletions
+39
View File
@@ -1,4 +1,5 @@
//! Transcript gestures leave ordinary typing and composer editing with the existing input path.
//! Stationary link clicks open on release; dragging or scrolling keeps the gesture in selection.
use crate::key_hint::KeyBindingListExt;
use crossterm::event::KeyCode;
@@ -206,6 +207,27 @@ impl TranscriptView {
self.extend_selection(event.column, event.row);
}
MouseEventKind::Up(MouseButton::Left) if dragging => {
// Open only a stationary click. Dragging back to the origin is still selection,
// and wheel scrolling clears the pointer even when it cannot move the viewport.
let link = self.selection.as_mut().and_then(|selection| {
(inside
&& !selection.moved
&& selection.pointer == Some(ScreenPosition::new(event.column, event.row))
&& event.modifiers.is_empty())
.then(|| selection.pressed_link.take())
.flatten()
});
let link = link.filter(|destination| {
self.visible
.get(usize::from(event.row - self.area.y))
.and_then(|visible| {
visible
.layout
.link_at(visible.row, event.column - self.area.x)
})
.as_ref()
== Some(destination)
});
if self
.selection
.as_ref()
@@ -217,6 +239,9 @@ impl TranscriptView {
if self.selected_text(cells).is_none() {
self.end_selection(cells);
}
if let Some(link) = link {
return Some(ViewAction::OpenLink(link));
}
}
_ => return None,
}
@@ -317,7 +342,21 @@ impl TranscriptView {
{
return None;
}
let link = (clicks == 1 && event.modifiers.is_empty())
.then(|| {
visible
.layout
.link_at(visible.row, event.column.saturating_sub(self.area.x))
})
.flatten();
self.begin_selection(cells, event.column, event.row, clicks);
if let Some(selection) = &mut self.selection {
selection.pressed_link = link;
}
Some(ViewAction::Changed)
}
}
#[cfg(test)]
#[path = "input_tests.rs"]
mod tests;