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
77 changes: 60 additions & 17 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6211,10 +6211,33 @@ impl Editor {
}

let display_snapshot = self.display_snapshot(cx);
let text_layout_details = self.text_layout_details(window, cx);

let font_id = text_layout_details
.text_system
.resolve_font(&text_layout_details.editor_style.text.font());
let font_size = text_layout_details
.editor_style
.text
.font_size
.to_pixels(text_layout_details.rem_size);
let Ok(space_width) = text_layout_details
.text_system
.advance(font_id, font_size, ' ')
.map(|advance| advance.width)
else {
return;
};
if space_width <= px(0.) {
return;
}
let buffer_snapshot = display_snapshot.buffer_snapshot();
let tab_size = display_snapshot.tab_snapshot().tab_size.get();

struct CursorData {
anchor: Anchor,
point: Point,
row: u32,
x: Pixels,
}
let cursor_data: Vec<CursorData> = self
.selections
Expand All @@ -6226,40 +6249,60 @@ impl Editor {
} else {
selection.tail()
};
let point = anchor.to_point(buffer_snapshot);
let mut prefix = String::new();
let mut column = 0;
for chunk in buffer_snapshot.text_for_range(Point::new(point.row, 0)..point) {
for ch in chunk.chars() {
if ch == '\t' {
let tab_len = tab_size - column % tab_size;
prefix.extend(iter::repeat_n(' ', tab_len as usize));
column += tab_len;
} else {
prefix.push(ch);
column += 1;
}
}
}
let run = text_layout_details.editor_style.text.to_run(prefix.len());
let x = text_layout_details
.text_system
.layout_line(&prefix, font_size, &[run], None)
.width;
CursorData {
anchor: anchor,
point: anchor.to_point(&display_snapshot.buffer_snapshot()),
anchor,
row: point.row,
x,
}
})
.collect();

let rows_anchors_count: Vec<usize> = cursor_data
.iter()
.map(|cursor| cursor.point.row)
.map(|cursor| cursor.row)
.chunk_by(|&row| row)
.into_iter()
.map(|(_, group)| group.count())
.collect();
let max_columns = rows_anchors_count.iter().max().copied().unwrap_or(0);
let mut rows_column_offset = vec![0; rows_anchors_count.len()];
let mut rows_x_offset = vec![px(0.); rows_anchors_count.len()];
let mut edits = Vec::new();

for column_idx in 0..max_columns {
let mut cursor_index = 0;

// Calculate target_column => position that the selections will go
let mut target_column = 0;
// Calculate target_x => position that the selections will go
let mut target_x = px(0.);
for (row_idx, cursor_count) in rows_anchors_count.iter().enumerate() {
// Skip rows that don't have this column
if column_idx >= *cursor_count {
cursor_index += cursor_count;
continue;
}

let point = &cursor_data[cursor_index + column_idx].point;
let adjusted_column = point.column + rows_column_offset[row_idx];
if adjusted_column > target_column {
target_column = adjusted_column;
let adjusted_x = cursor_data[cursor_index + column_idx].x + rows_x_offset[row_idx];
if adjusted_x > target_x {
target_x = adjusted_x;
}
cursor_index += cursor_count;
}
Expand All @@ -6273,15 +6316,15 @@ impl Editor {
continue;
}

let point = &cursor_data[cursor_index + column_idx].point;
let spaces_needed = target_column - point.column - rows_column_offset[row_idx];
let cursor = &cursor_data[cursor_index + column_idx];
let spaces_needed = ((target_x - cursor.x - rows_x_offset[row_idx]) / space_width)
.round()
.max(0.) as u32;
if spaces_needed > 0 {
let anchor = cursor_data[cursor_index + column_idx]
.anchor
.bias_left(&display_snapshot);
let anchor = cursor.anchor.bias_left(&display_snapshot);
edits.push((anchor..anchor, " ".repeat(spaces_needed as usize)));
}
rows_column_offset[row_idx] += spaces_needed;
rows_x_offset[row_idx] += space_width * spaces_needed as f32;

cursor_index += *cursor_count;
}
Expand Down
99 changes: 99 additions & 0 deletions crates/editor/src/editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ use settings::{
};
use std::{
borrow::Cow,
cmp::Ordering,
sync::{Arc, atomic},
};
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
Expand Down Expand Up @@ -3332,6 +3333,44 @@ async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
});
}

#[gpui::test]
async fn test_newest_selection_on_screen_with_multibyte_chars(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;

let multibyte_line = "ã".repeat(40);
cx.set_state(&format!("{multibyte_line}ˇx\n"));
cx.update_editor(|editor, window, cx| {
editor.set_visible_line_count(50., window, cx);
editor.set_visible_column_count(60.);
assert_eq!(
editor.newest_selection_on_screen(window, cx),
Ordering::Equal
);
editor.set_visible_column_count(20.);
assert_eq!(
editor.newest_selection_on_screen(window, cx),
Ordering::Greater
);
});

let ascii_line = "a".repeat(40);
cx.set_state(&format!("{ascii_line}ˇx\n"));
cx.update_editor(|editor, window, cx| {
editor.set_visible_line_count(50., window, cx);
editor.set_visible_column_count(60.);
assert_eq!(
editor.newest_selection_on_screen(window, cx),
Ordering::Equal
);
editor.set_visible_column_count(20.);
assert_eq!(
editor.newest_selection_on_screen(window, cx),
Ordering::Greater
);
});
}

#[gpui::test]
async fn test_scroll_line_up_down_cursor_margin(cx: &mut TestAppContext) {
init_test(cx, |_| {});
Expand Down Expand Up @@ -41838,6 +41877,66 @@ async fn test_align_selections_multicolumn(cx: &mut TestAppContext) {
cx.assert_editor_state(after);
}

#[gpui::test]
async fn test_align_selections_with_multibyte_chars(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;

// The reported case: `←` and `π` take more bytes than they take columns, so
// aligning on the buffer column padded the first row one space too far.
let before = "a ← 1 ˇ# one\nbc ← π ˇ# two";
let after = "a ← 1 ˇ# one\nbc ← π ˇ# two";
cx.set_state(before);
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
cx.assert_editor_state(after);

// A multi-byte character before the first column also has to shift the
// offset that is carried into the second column.
let before = "π aˇ bbbˇc\nxy aˇ bˇc";
let after = "π a ˇ bbbˇc\nxy aˇ b ˇc";
cx.set_state(before);
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
cx.assert_editor_state(after);

// The display map expands tabs before the row is laid out, so a leading tab
// counts as its expanded width rather than as one byte.
let before = "\taˇbc\nxyzaˇbc";
let after = "\taˇbc\nxyza ˇbc";
cx.set_state(before);
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
cx.assert_editor_state(after);

// A non-BMP character advances two columns, so counting characters rather
// than measuring advances would pad this row twice as far as it needs.
let before = "😀ˇb\nxyzˇb";
let after = "😀 ˇb\nxyzˇb";
cx.set_state(before);
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
cx.assert_editor_state(after);

// Multi-byte characters after the cursors do not move them.
let before = "abˇ←z\ncdˇqz";
cx.set_state(before);
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
cx.assert_editor_state(before);
}

#[gpui::test]
async fn test_align_selections_with_soft_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;

let before = "aaaaaaaaaaaaˇx\nbbbbbˇy";
let after = "aaaaaaaaaaaaˇx\nbbbbb ˇy";
cx.set_state(before);
cx.update_editor(|e, _, cx| e.set_wrap_width(Some(100.0.into()), cx));
cx.update_editor(|e, window, cx| {
assert!(e.display_text(cx).lines().count() > 2);
e.align_selections(&AlignSelections, window, cx)
});
cx.assert_editor_state(after);
}

#[gpui::test]
async fn test_custom_fallback_highlights(cx: &mut TestAppContext) {
init_test(cx, |_| {});
Expand Down
29 changes: 26 additions & 3 deletions crates/editor/src/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,7 @@ impl Editor {
/// Ordering::Equal => on screen
/// Ordering::Less => above or to the left of the screen
/// Ordering::Greater => below or to the right of the screen
pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
pub fn newest_selection_on_screen(&self, window: &mut Window, cx: &mut App) -> Ordering {
let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let newest_head = self
.selections
Expand All @@ -938,9 +938,32 @@ impl Editor {
if let (Some(visible_lines), Some(visible_columns)) =
(self.visible_line_count(), self.visible_column_count())
&& newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
&& newest_head.column() <= screen_top.column() + visible_columns as u32
{
return Ordering::Equal;
let text_layout_details = self.text_layout_details(window, cx);
let font_id = text_layout_details
.text_system
.resolve_font(&text_layout_details.editor_style.text.font());
let font_size = text_layout_details
.editor_style
.text
.font_size
.to_pixels(text_layout_details.rem_size);
let on_screen = match text_layout_details
.text_system
.em_advance(font_id, font_size)
.log_err()
{
Some(em_advance) => {
let head_x = snapshot.x_for_display_point(newest_head, &text_layout_details);
let screen_left_x =
snapshot.x_for_display_point(screen_top, &text_layout_details);
head_x <= screen_left_x + em_advance * visible_columns as f32
}
None => newest_head.column() <= screen_top.column() + visible_columns as u32,
};
if on_screen {
return Ordering::Equal;
}
}

Ordering::Greater
Expand Down
2 changes: 1 addition & 1 deletion crates/vim/src/normal/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl Vim {
cx: &mut Context<Vim>,
) {
self.update_editor(cx, |vim, editor, cx| {
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
let should_move_cursor = editor.newest_selection_on_screen(window, cx).is_eq();
let display_snapshot = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
let old_top = editor.scroll_top_display_point(&display_snapshot, cx);

Expand Down
8 changes: 7 additions & 1 deletion typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ extend-ignore-re = [
# Mermaid CSS class name for state diagram composites
"composit",
# Used in truncating tests to ensure byte-length vs char like in café
"caf…"
"caf…",
'\[elete\]',
"GHIzJ",
]
check-filename = true

[default.extend-words]
scap = "scap"
writeable = "writeable"
Loading