Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions crates/ui/src/markdown/selection_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use std::{
};

use gpui::{
AnyWindowHandle, App, AppContext as _, Bounds, EntityId, Hitbox, Hsla, Modifiers, Pixels,
PlatformInput, Point, ScrollDelta, ScrollWheelEvent, Task, TextLayout, WeakEntity, Window,
point, px,
AnyWindowHandle, App, AppContext as _, Bounds, EntityId, Half as _, Hitbox, Hsla, Modifiers,
Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, Task, TextLayout, WeakEntity,
Window, point, px,
};
use gpui_base::{
TextSelectionContentKey, TextSelectionCoverage, TextSelectionEndpoint, TextSelectionEvent,
Expand Down Expand Up @@ -355,7 +355,7 @@ impl MarkdownSelectionAdapter {
}

/// The inline states painted last frame in rows at `window_point`'s
/// height, with the text offset nearest to it in each.
/// height, with the character boundary nearest to it in each.
pub(super) fn runs_at(
&self,
window_point: Point<Pixels>,
Expand All @@ -369,13 +369,7 @@ impl MarkdownSelectionAdapter {
let bounds = run.bounds();
bounds.top() <= window_point.y && window_point.y < bounds.bottom()
})
.map(|(run, state)| {
let offset = run
.layout()
.index_for_position(window_point)
.unwrap_or_else(|nearest| nearest);
(state.clone(), offset)
})
.map(|(run, state)| (state.clone(), nearest_boundary(run, window_point)))
.collect()
}

Expand Down Expand Up @@ -443,6 +437,40 @@ impl MarkdownSelectionAdapter {
}
}

/// The character boundary in `run` nearest to `window_point`.
///
/// gpui-base paints a character as selected when the selection covers its
/// horizontal midpoint, while `TextLayout::index_for_position` returns the
/// start of the character under the point, so an end on a character's right
/// half would copy one character less than is painted. The midpoint and the
/// width of a character that ends its row follow gpui-base's band test.
fn nearest_boundary(run: &TextSelectionRun, window_point: Point<Pixels>) -> usize {
let layout = run.layout();
let offset = match layout.index_for_position(window_point) {
Ok(offset) => offset,
Err(nearest) => return nearest,
};
let Some(next_offset) = run.text()[offset..]
.chars()
.next()
.map(|character| offset + character.len_utf8())
else {
return offset;
};
let Some(position) = layout.position_for_index(offset) else {
return offset;
};
let width = layout
.position_for_index(next_offset)
.filter(|next| next.y == position.y)
.map_or_else(|| layout.line_height().half(), |next| next.x - position.x);
if window_point.x >= position.x + width.half() {
next_offset
} else {
offset
}
}

fn start_auto_scroll(state: &Rc<RefCell<AutoScrollState>>, cx: &mut App) {
if state.borrow().task.is_some() {
return;
Expand Down
27 changes: 27 additions & 0 deletions crates/ui/src/markdown/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,4 +791,31 @@ mod tests {
"intro\nfirst line\nsecond line\nname value\nalpha beta\n"
);
}

#[gpui::test]
fn drag_copies_exactly_the_highlighted_text(cx: &mut TestAppContext) {
use gpui::{Modifiers, MouseButton, point};

cx.update(crate::theme::init);
cx.update(super::super::init);
let (view, cx) = cx.add_window_view(|_, cx| SelectAllRoot::new("Hello world", cx));
let cx: &mut VisualTestContext = cx;
cx.update(|window, cx| _ = window.draw(cx));

// The sweep ends the drag on both halves of every character it crosses.
for x in 1..120 {
let (start, end) = (point(px(1.), px(12.)), point(px(x as f32), px(12.)));
cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
cx.update(|window, cx| _ = window.draw(cx));
cx.simulate_mouse_move(end, Some(MouseButton::Left), Modifiers::default());
cx.update(|window, cx| _ = window.draw(cx));
cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::default());
cx.update(|window, cx| _ = window.draw(cx));

let highlighted =
view.read_with(cx, |root, cx| root.markdown.read(cx).parsed.selected_text());
let copied = cx.update(gpui_base::TextSelection::selected_text);
assert_eq!(copied, highlighted, "drag to x = {x}");
}
}
}
Loading