Fix line number whitespace (#20427)
Closes #14025 https://github.com/user-attachments/assets/24b3f321-8246-4203-9510-66a7cf3d22f0 Release Notes: - Fixed bug where toggling line numbers would incorrectly hide whitespace indicators.
This commit is contained in:
parent
8bc5bcf0a6
commit
01503511ad
1 changed files with 100 additions and 63 deletions
|
@ -1968,10 +1968,10 @@ impl EditorElement {
|
||||||
|
|
||||||
fn layout_lines(
|
fn layout_lines(
|
||||||
rows: Range<DisplayRow>,
|
rows: Range<DisplayRow>,
|
||||||
line_number_layouts: &[Option<ShapedLine>],
|
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
editor_width: Pixels,
|
editor_width: Pixels,
|
||||||
|
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Vec<LineWithInvisibles> {
|
) -> Vec<LineWithInvisibles> {
|
||||||
if rows.start >= rows.end {
|
if rows.start >= rows.end {
|
||||||
|
@ -2020,9 +2020,9 @@ impl EditorElement {
|
||||||
&style.text,
|
&style.text,
|
||||||
MAX_LINE_LEN,
|
MAX_LINE_LEN,
|
||||||
rows.len(),
|
rows.len(),
|
||||||
line_number_layouts,
|
|
||||||
snapshot.mode,
|
snapshot.mode,
|
||||||
editor_width,
|
editor_width,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2071,6 +2071,7 @@ impl EditorElement {
|
||||||
scroll_width: &mut Pixels,
|
scroll_width: &mut Pixels,
|
||||||
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
||||||
selections: &[Selection<Point>],
|
selections: &[Selection<Point>],
|
||||||
|
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (AnyElement, Size<Pixels>) {
|
) -> (AnyElement, Size<Pixels>) {
|
||||||
let mut element = match block {
|
let mut element = match block {
|
||||||
|
@ -2083,8 +2084,15 @@ impl EditorElement {
|
||||||
line_layouts[align_to.row().minus(rows.start) as usize]
|
line_layouts[align_to.row().minus(rows.start) as usize]
|
||||||
.x_for_index(align_to.column() as usize)
|
.x_for_index(align_to.column() as usize)
|
||||||
} else {
|
} else {
|
||||||
layout_line(align_to.row(), snapshot, &self.style, editor_width, cx)
|
layout_line(
|
||||||
.x_for_index(align_to.column() as usize)
|
align_to.row(),
|
||||||
|
snapshot,
|
||||||
|
&self.style,
|
||||||
|
editor_width,
|
||||||
|
is_row_soft_wrapped,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.x_for_index(align_to.column() as usize)
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected = selections
|
let selected = selections
|
||||||
|
@ -2447,6 +2455,7 @@ impl EditorElement {
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
line_layouts: &[LineWithInvisibles],
|
line_layouts: &[LineWithInvisibles],
|
||||||
selections: &[Selection<Point>],
|
selections: &[Selection<Point>],
|
||||||
|
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
|
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
|
||||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||||
|
@ -2484,6 +2493,7 @@ impl EditorElement {
|
||||||
scroll_width,
|
scroll_width,
|
||||||
&mut resized_blocks,
|
&mut resized_blocks,
|
||||||
selections,
|
selections,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
||||||
|
@ -2529,6 +2539,7 @@ impl EditorElement {
|
||||||
scroll_width,
|
scroll_width,
|
||||||
&mut resized_blocks,
|
&mut resized_blocks,
|
||||||
selections,
|
selections,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2575,6 +2586,7 @@ impl EditorElement {
|
||||||
scroll_width,
|
scroll_width,
|
||||||
&mut resized_blocks,
|
&mut resized_blocks,
|
||||||
selections,
|
selections,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -4359,9 +4371,9 @@ impl LineWithInvisibles {
|
||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
max_line_len: usize,
|
max_line_len: usize,
|
||||||
max_line_count: usize,
|
max_line_count: usize,
|
||||||
line_number_layouts: &[Option<ShapedLine>],
|
|
||||||
editor_mode: EditorMode,
|
editor_mode: EditorMode,
|
||||||
text_width: Pixels,
|
text_width: Pixels,
|
||||||
|
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
let mut layouts = Vec::with_capacity(max_line_count);
|
let mut layouts = Vec::with_capacity(max_line_count);
|
||||||
|
@ -4489,12 +4501,9 @@ impl LineWithInvisibles {
|
||||||
if editor_mode == EditorMode::Full {
|
if editor_mode == EditorMode::Full {
|
||||||
// Line wrap pads its contents with fake whitespaces,
|
// Line wrap pads its contents with fake whitespaces,
|
||||||
// avoid printing them
|
// avoid printing them
|
||||||
let inside_wrapped_string = line_number_layouts
|
let is_soft_wrapped = is_row_soft_wrapped(row);
|
||||||
.get(row)
|
|
||||||
.and_then(|layout| layout.as_ref())
|
|
||||||
.is_none();
|
|
||||||
if highlighted_chunk.is_tab {
|
if highlighted_chunk.is_tab {
|
||||||
if non_whitespace_added || !inside_wrapped_string {
|
if non_whitespace_added || !is_soft_wrapped {
|
||||||
invisibles.push(Invisible::Tab {
|
invisibles.push(Invisible::Tab {
|
||||||
line_start_offset: line.len(),
|
line_start_offset: line.len(),
|
||||||
line_end_offset: line.len() + line_chunk.len(),
|
line_end_offset: line.len() + line_chunk.len(),
|
||||||
|
@ -4510,7 +4519,7 @@ impl LineWithInvisibles {
|
||||||
(*line_byte as char).is_whitespace();
|
(*line_byte as char).is_whitespace();
|
||||||
non_whitespace_added |= !is_whitespace;
|
non_whitespace_added |= !is_whitespace;
|
||||||
is_whitespace
|
is_whitespace
|
||||||
&& (non_whitespace_added || !inside_wrapped_string)
|
&& (non_whitespace_added || !is_soft_wrapped)
|
||||||
})
|
})
|
||||||
.map(|(whitespace_index, _)| Invisible::Whitespace {
|
.map(|(whitespace_index, _)| Invisible::Whitespace {
|
||||||
line_offset: line.len() + whitespace_index,
|
line_offset: line.len() + whitespace_index,
|
||||||
|
@ -4873,10 +4882,10 @@ impl Element for EditorElement {
|
||||||
editor_handle.update(cx, |editor, cx| editor.snapshot(cx));
|
editor_handle.update(cx, |editor, cx| editor.snapshot(cx));
|
||||||
let line = Self::layout_lines(
|
let line = Self::layout_lines(
|
||||||
DisplayRow(0)..DisplayRow(1),
|
DisplayRow(0)..DisplayRow(1),
|
||||||
&[],
|
|
||||||
&editor_snapshot,
|
&editor_snapshot,
|
||||||
&style,
|
&style,
|
||||||
px(f32::MAX),
|
px(f32::MAX),
|
||||||
|
|_| false, // Single lines never soft wrap
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.pop()
|
.pop()
|
||||||
|
@ -5085,6 +5094,8 @@ impl Element for EditorElement {
|
||||||
.buffer_rows(start_row)
|
.buffer_rows(start_row)
|
||||||
.take((start_row..end_row).len())
|
.take((start_row..end_row).len())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let is_row_soft_wrapped =
|
||||||
|
|row| buffer_rows.get(row).copied().flatten().is_none();
|
||||||
|
|
||||||
let start_anchor = if start_row == Default::default() {
|
let start_anchor = if start_row == Default::default() {
|
||||||
Anchor::min()
|
Anchor::min()
|
||||||
|
@ -5176,10 +5187,10 @@ impl Element for EditorElement {
|
||||||
let mut max_visible_line_width = Pixels::ZERO;
|
let mut max_visible_line_width = Pixels::ZERO;
|
||||||
let mut line_layouts = Self::layout_lines(
|
let mut line_layouts = Self::layout_lines(
|
||||||
start_row..end_row,
|
start_row..end_row,
|
||||||
&line_numbers,
|
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&self.style,
|
&self.style,
|
||||||
editor_width,
|
editor_width,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
for line_with_invisibles in &line_layouts {
|
for line_with_invisibles in &line_layouts {
|
||||||
|
@ -5188,9 +5199,15 @@ impl Element for EditorElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let longest_line_width =
|
let longest_line_width = layout_line(
|
||||||
layout_line(snapshot.longest_row(), &snapshot, &style, editor_width, cx)
|
snapshot.longest_row(),
|
||||||
.width;
|
&snapshot,
|
||||||
|
&style,
|
||||||
|
editor_width,
|
||||||
|
is_row_soft_wrapped,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.width;
|
||||||
let mut scroll_width =
|
let mut scroll_width =
|
||||||
longest_line_width.max(max_visible_line_width) + overscroll.width;
|
longest_line_width.max(max_visible_line_width) + overscroll.width;
|
||||||
|
|
||||||
|
@ -5208,6 +5225,7 @@ impl Element for EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
&line_layouts,
|
&line_layouts,
|
||||||
&local_selections,
|
&local_selections,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -5966,6 +5984,7 @@ fn layout_line(
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
text_width: Pixels,
|
text_width: Pixels,
|
||||||
|
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> LineWithInvisibles {
|
) -> LineWithInvisibles {
|
||||||
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
||||||
|
@ -5974,9 +5993,9 @@ fn layout_line(
|
||||||
&style.text,
|
&style.text,
|
||||||
MAX_LINE_LEN,
|
MAX_LINE_LEN,
|
||||||
1,
|
1,
|
||||||
&[],
|
|
||||||
snapshot.mode,
|
snapshot.mode,
|
||||||
text_width,
|
text_width,
|
||||||
|
is_row_soft_wrapped,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.pop()
|
.pop()
|
||||||
|
@ -6661,15 +6680,22 @@ mod tests {
|
||||||
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
||||||
);
|
);
|
||||||
|
|
||||||
init_test(cx, |s| {
|
for show_line_numbers in [true, false] {
|
||||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
init_test(cx, |s| {
|
||||||
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
|
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||||
});
|
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
|
||||||
|
});
|
||||||
|
|
||||||
let actual_invisibles =
|
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||||
collect_invisibles_from_new_editor(cx, EditorMode::Full, input_text, px(500.0));
|
cx,
|
||||||
|
EditorMode::Full,
|
||||||
|
input_text,
|
||||||
|
px(500.0),
|
||||||
|
show_line_numbers,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(expected_invisibles, actual_invisibles);
|
assert_eq!(expected_invisibles, actual_invisibles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -6683,14 +6709,17 @@ mod tests {
|
||||||
EditorMode::SingleLine { auto_width: false },
|
EditorMode::SingleLine { auto_width: false },
|
||||||
EditorMode::AutoHeight { max_lines: 100 },
|
EditorMode::AutoHeight { max_lines: 100 },
|
||||||
] {
|
] {
|
||||||
let invisibles = collect_invisibles_from_new_editor(
|
for show_line_numbers in [true, false] {
|
||||||
cx,
|
let invisibles = collect_invisibles_from_new_editor(
|
||||||
editor_mode_without_invisibles,
|
cx,
|
||||||
"\t\t\t| | a b",
|
editor_mode_without_invisibles,
|
||||||
px(500.0),
|
"\t\t\t| | a b",
|
||||||
);
|
px(500.0),
|
||||||
assert!(invisibles.is_empty(),
|
show_line_numbers,
|
||||||
|
);
|
||||||
|
assert!(invisibles.is_empty(),
|
||||||
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
|
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6741,43 +6770,48 @@ mod tests {
|
||||||
let resize_step = 10.0;
|
let resize_step = 10.0;
|
||||||
let mut editor_width = 200.0;
|
let mut editor_width = 200.0;
|
||||||
while editor_width <= 1000.0 {
|
while editor_width <= 1000.0 {
|
||||||
update_test_language_settings(cx, |s| {
|
for show_line_numbers in [true, false] {
|
||||||
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
update_test_language_settings(cx, |s| {
|
||||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
||||||
s.defaults.preferred_line_length = Some(editor_width as u32);
|
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||||
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
|
s.defaults.preferred_line_length = Some(editor_width as u32);
|
||||||
});
|
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
|
||||||
|
});
|
||||||
|
|
||||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||||
cx,
|
cx,
|
||||||
EditorMode::Full,
|
EditorMode::Full,
|
||||||
&input_text,
|
&input_text,
|
||||||
px(editor_width),
|
px(editor_width),
|
||||||
);
|
show_line_numbers,
|
||||||
|
);
|
||||||
|
|
||||||
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
|
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
|
||||||
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
|
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
|
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
|
||||||
i = actual_index;
|
i = actual_index;
|
||||||
match expected_invisibles.get(i) {
|
match expected_invisibles.get(i) {
|
||||||
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
|
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
|
||||||
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
|
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
|
||||||
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
|
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
|
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
|
|
||||||
}
|
}
|
||||||
}
|
let missing_expected_invisibles = &expected_invisibles[i + 1..];
|
||||||
let missing_expected_invisibles = &expected_invisibles[i + 1..];
|
assert!(
|
||||||
assert!(
|
missing_expected_invisibles.is_empty(),
|
||||||
missing_expected_invisibles.is_empty(),
|
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
|
||||||
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
|
);
|
||||||
);
|
|
||||||
|
|
||||||
editor_width += resize_step;
|
editor_width += resize_step;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6786,6 +6820,7 @@ mod tests {
|
||||||
editor_mode: EditorMode,
|
editor_mode: EditorMode,
|
||||||
input_text: &str,
|
input_text: &str,
|
||||||
editor_width: Pixels,
|
editor_width: Pixels,
|
||||||
|
show_line_numbers: bool,
|
||||||
) -> Vec<Invisible> {
|
) -> Vec<Invisible> {
|
||||||
info!(
|
info!(
|
||||||
"Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
|
"Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
|
||||||
|
@ -6797,11 +6832,13 @@ mod tests {
|
||||||
});
|
});
|
||||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||||
let editor = window.root(cx).unwrap();
|
let editor = window.root(cx).unwrap();
|
||||||
|
|
||||||
let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
|
let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
|
||||||
window
|
window
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||||
editor.set_wrap_width(Some(editor_width), cx);
|
editor.set_wrap_width(Some(editor_width), cx);
|
||||||
|
editor.set_show_line_numbers(show_line_numbers, cx);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
|
let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue