Fix whitespace rendering in Zed (#54798)
Closes https://github.com/zed-industries/zed/issues/49186 <img width="403" height="136" alt="first" src="https://github.com/user-attachments/assets/dafaa681-b921-4411-a453-8857681e50f8" /> Closes https://github.com/zed-industries/zed/issues/45775 <img width="405" height="104" alt="second" src="https://github.com/user-attachments/assets/17e3b5df-0d61-47f3-88ff-8aeeafae1425" /> Release Notes: - Fixed whitespace rendering in Zed
This commit is contained in:
1 parent
83830be2b1
commit
cecd991263
1 file changed
+78
-20
@@ -8963,6 +8963,10 @@ impl LineWithInvisibles {
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
|
||||
let mut line = String::new();
|
||||
// Byte offset into the logical line used to position invisible markers.
|
||||
// Unlike `line`, this is not cleared when we flush `shape_line` for
|
||||
// mid-line inlays/replacements, so marker offsets stay correct in that case.
|
||||
let mut line_byte_offset: usize = 0;
|
||||
let mut invisibles = Vec::new();
|
||||
let mut width = Pixels::ZERO;
|
||||
let mut len = 0;
|
||||
@@ -9036,6 +9040,7 @@ impl LineWithInvisibles {
|
||||
|
||||
width += size.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
line_byte_offset += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Element {
|
||||
id: renderer.id,
|
||||
element: Some(element),
|
||||
@@ -9065,6 +9070,7 @@ impl LineWithInvisibles {
|
||||
|
||||
width += line_layout.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
line_byte_offset += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Text(line_layout))
|
||||
}
|
||||
}
|
||||
@@ -9095,6 +9101,7 @@ impl LineWithInvisibles {
|
||||
});
|
||||
|
||||
line.clear();
|
||||
line_byte_offset = 0;
|
||||
styles.clear();
|
||||
row += 1;
|
||||
line_exceeded_max_len = false;
|
||||
@@ -9136,8 +9143,8 @@ impl LineWithInvisibles {
|
||||
if highlighted_chunk.is_tab {
|
||||
if non_whitespace_added || !is_soft_wrapped {
|
||||
invisibles.push(Invisible::Tab {
|
||||
line_start_offset: line.len(),
|
||||
line_end_offset: line.len() + line_chunk.len(),
|
||||
line_start_offset: line_byte_offset,
|
||||
line_end_offset: line_byte_offset + line_chunk.len(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -9149,7 +9156,10 @@ impl LineWithInvisibles {
|
||||
&& (non_whitespace_added || !is_soft_wrapped)
|
||||
{
|
||||
Some(Invisible::Whitespace {
|
||||
line_offset: line.len() + index,
|
||||
line_start_offset: line_byte_offset + index,
|
||||
line_end_offset: line_byte_offset
|
||||
+ index
|
||||
+ c.len_utf8(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -9160,6 +9170,7 @@ impl LineWithInvisibles {
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
line_byte_offset += line_chunk.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9431,15 +9442,23 @@ impl LineWithInvisibles {
|
||||
line_start_offset,
|
||||
line_end_offset,
|
||||
} => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
|
||||
Invisible::Whitespace { line_offset } => {
|
||||
(*line_offset, line_offset + 1, &layout.space_invisible)
|
||||
}
|
||||
Invisible::Whitespace {
|
||||
line_start_offset,
|
||||
line_end_offset,
|
||||
} => (
|
||||
*line_start_offset,
|
||||
*line_end_offset,
|
||||
&layout.space_invisible,
|
||||
),
|
||||
};
|
||||
|
||||
let x_offset: ScrollPixelOffset = self.x_for_index(token_offset).into();
|
||||
let token_x = self.x_for_index(token_offset);
|
||||
// Center the marker inside the actual glyph's width so it lines up with
|
||||
// proportional fonts instead of assuming a monospace `em_width` cell.
|
||||
let glyph_width = (self.x_for_index(token_end_offset) - token_x).max(Pixels::ZERO);
|
||||
let x_offset: ScrollPixelOffset = token_x.into();
|
||||
let invisible_offset: ScrollPixelOffset =
|
||||
((layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0)
|
||||
.into();
|
||||
((glyph_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0).into();
|
||||
let origin = content_origin
|
||||
+ gpui::point(
|
||||
Pixels::from(
|
||||
@@ -9635,8 +9654,13 @@ enum Invisible {
|
||||
line_start_offset: usize,
|
||||
line_end_offset: usize,
|
||||
},
|
||||
/// A whitespace character (ASCII space or any other Unicode whitespace).
|
||||
///
|
||||
/// Storing both offsets correctly accounts for multi-byte whitespace characters
|
||||
/// such as U+00A0 NO-BREAK SPACE, keeping adjacency checks correct.
|
||||
Whitespace {
|
||||
line_offset: usize,
|
||||
line_start_offset: usize,
|
||||
line_end_offset: usize,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -11201,7 +11225,6 @@ impl Element for EditorElement {
|
||||
scroll_max,
|
||||
line_layouts,
|
||||
line_height,
|
||||
em_width,
|
||||
em_advance,
|
||||
em_layout_width,
|
||||
snapshot,
|
||||
@@ -12085,7 +12108,6 @@ pub(crate) struct PositionMap {
|
||||
pub scroll_position: gpui::Point<ScrollOffset>,
|
||||
pub scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||||
pub scroll_max: gpui::Point<ScrollOffset>,
|
||||
pub em_width: Pixels,
|
||||
pub em_advance: Pixels,
|
||||
pub em_layout_width: Pixels,
|
||||
pub visible_row_range: Range<DisplayRow>,
|
||||
@@ -13424,7 +13446,8 @@ mod tests {
|
||||
line_end_offset: TAB_SIZE as usize,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize,
|
||||
line_start_offset: TAB_SIZE as usize,
|
||||
line_end_offset: TAB_SIZE as usize + 1,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: TAB_SIZE as usize + 1,
|
||||
@@ -13435,10 +13458,12 @@ mod tests {
|
||||
line_end_offset: TAB_SIZE as usize * 3,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize * 3 + 1,
|
||||
line_start_offset: TAB_SIZE as usize * 3 + 1,
|
||||
line_end_offset: TAB_SIZE as usize * 3 + 2,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize * 3 + 3,
|
||||
line_start_offset: TAB_SIZE as usize * 3 + 3,
|
||||
line_end_offset: TAB_SIZE as usize * 3 + 4,
|
||||
},
|
||||
];
|
||||
assert_eq!(
|
||||
@@ -13468,6 +13493,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_multibyte_whitespace_uses_utf8_byte_offsets(cx: &mut TestAppContext) {
|
||||
init_test(cx, |s| {
|
||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||
});
|
||||
|
||||
// Regression test for #49186. NBSP (U+00A0) is rendered via the invisible
|
||||
// character `replacement` pipeline, which flushes the internal `line`
|
||||
// scratch buffer mid-line. Any whitespace invisible that follows must use
|
||||
// the absolute byte offset within the logical line (here: byte 4 for the
|
||||
// trailing ASCII space), not an offset relative to the post-flush buffer.
|
||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
EditorMode::full(),
|
||||
"a\u{00A0}b ",
|
||||
px(500.0),
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual_invisibles,
|
||||
vec![Invisible::Whitespace {
|
||||
line_start_offset: 4,
|
||||
line_end_offset: 5,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
|
||||
init_test(cx, |s| {
|
||||
@@ -13508,19 +13561,24 @@ mod tests {
|
||||
line_end_offset: tab_size as usize,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 3,
|
||||
line_start_offset: tab_size as usize + 3,
|
||||
line_end_offset: tab_size as usize + 4,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 4,
|
||||
line_start_offset: tab_size as usize + 4,
|
||||
line_end_offset: tab_size as usize + 5,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 5,
|
||||
line_start_offset: tab_size as usize + 5,
|
||||
line_end_offset: tab_size as usize + 6,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 6,
|
||||
line_start_offset: tab_size as usize + 6,
|
||||
line_end_offset: tab_size as usize + 7,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 7,
|
||||
line_start_offset: tab_size as usize + 7,
|
||||
line_end_offset: tab_size as usize + 8,
|
||||
},
|
||||
];
|
||||
let expected_invisibles = std::iter::once(repeated_invisibles)
|
||||
|
||||
Reference in new issue
Block a user