diff --git a/src/editor/buffer.rs b/src/editor/buffer.rs index a5ca6e7..418dd39 100644 --- a/src/editor/buffer.rs +++ b/src/editor/buffer.rs @@ -1,10 +1,28 @@ -use std::ops::Range; +use std::{ + ops::Range, + sync::atomic::{AtomicU64, Ordering}, +}; /// Source text and where each line starts, in bytes. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct Buffer { text: String, starts: Vec, + stamp: u64, +} + +impl PartialEq for Buffer { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + } +} + +impl Eq for Buffer {} + +/// A fresh stamp: no two texts share one. +fn stamp() -> u64 { + static NEXT: AtomicU64 = AtomicU64::new(1); + NEXT.fetch_add(1, Ordering::Relaxed) } fn starts(text: &str) -> Vec { @@ -17,7 +35,16 @@ impl Buffer { pub fn new(text: impl Into) -> Self { let text = text.into(); let starts = starts(&text); - Self { text, starts } + Self { + text, + starts, + stamp: stamp(), + } + } + + /// Names this text; every edit gives a new one. + pub(crate) fn stamp(&self) -> u64 { + self.stamp } pub fn text(&self) -> &str { @@ -107,6 +134,7 @@ impl Buffer { ); self.text.replace_range(range, text); self.starts = starts(&self.text); + self.stamp = stamp(); } } diff --git a/src/editor/line.rs b/src/editor/line.rs index 626b834..f0b7d1a 100644 --- a/src/editor/line.rs +++ b/src/editor/line.rs @@ -34,11 +34,13 @@ impl CodeEditor { span.start < range.end.max(range.start + 1) && range.start < span.end }; let mut spans = self.line_colors(self.row_text(line), cx); + // Offsets found in an older text are left out. + let fresh = self.frame.stamp == self.buffer.stamp(); let brackets = self .frame .brackets .iter() - .filter(|bracket| range.contains(&bracket.offset)); + .filter(|bracket| fresh && range.contains(&bracket.offset)); for bracket in brackets { let at = bracket.offset - range.start; let color = if self.options.rainbow { @@ -49,7 +51,12 @@ impl CodeEditor { spans.retain(|(span, _)| *span != (at..at + 1)); spans.push((at..at + 1, color.into())); } - for span in self.frame.occurrences.iter().filter(|span| touches(span)) { + for span in self + .frame + .occurrences + .iter() + .filter(|span| fresh && touches(span)) + { spans.push(( local(span), HighlightStyle { @@ -90,7 +97,7 @@ impl CodeEditor { }, )); } - if let Some((open, close)) = self.frame.matched { + if let Some((open, close)) = self.frame.matched.filter(|_| fresh) { for at in [open, close].into_iter().filter(|at| range.contains(at)) { let at = at - range.start; spans.push(( @@ -114,7 +121,7 @@ impl CodeEditor { (0..indent).step_by(self.options.tab).collect() } - /// Dots for spaces, arrows for tabs; the rest hidden, so marks fall where drawn. + /// Dots for spaces, arrows for tabs; other characters hidden. fn whitespace(&self, line: usize, cx: &Context) -> StyledText { let text = self.row_text(line); let notes = self.notes(line, cx); @@ -165,7 +172,7 @@ impl CodeEditor { advance, window, ); - // Text columns at their laid-out place; rulers and guides on the grid. + // Text sits where laid out; rulers and guides on the grid. let x = |column: usize| advance + measured.x(column); let grid = |column: usize| advance * (column + 1) as f32; let focused = self.focus.is_focused(window); diff --git a/src/editor/measure.rs b/src/editor/measure.rs index cfd7c79..ad6837a 100644 --- a/src/editor/measure.rs +++ b/src/editor/measure.rs @@ -1,4 +1,4 @@ -//! Where a drawn line's columns fall, as gpui lays it out. +//! Where drawn columns fall, as gpui lays them out. use std::sync::Arc; @@ -6,7 +6,7 @@ use gpui::{Hsla, LineLayout, Pixels, SharedString, TextRun, Window, font}; use super::layout::Shown; -/// A drawn line laid out: wide characters take their own width. +/// A laid-out line: wide characters keep their own width. pub(crate) struct Measured { text: SharedString, layout: Arc, @@ -57,7 +57,7 @@ impl Measured { } } - /// Where drawn column `column` starts; past the end in whole advances. + /// Where `column` starts; past the end in whole advances. pub fn x(&self, column: usize) -> Pixels { match self.text.char_indices().nth(column) { Some((at, _)) => self.layout.x_for_index(at), @@ -68,9 +68,19 @@ impl Measured { } } - /// The drawn column whose start is nearest `x`. + /// The column whose start lies nearest `x`, line end included. pub fn column(&self, x: Pixels) -> usize { - let at = self.layout.closest_index_for_x(x).min(self.text.len()); + let starts = self.layout.runs.iter().flat_map(|run| { + run.glyphs + .iter() + .map(|glyph| (glyph.index, glyph.position.x)) + }); + let off = |edge: Pixels| f32::from((edge - x).abs()); + let at = starts + .chain([(self.text.len(), self.layout.width)]) + .min_by(|a, b| off(a.1).total_cmp(&off(b.1))) + .map(|(at, _)| at.min(self.text.len())) + .expect("the line end is always a stop"); self.text[..at].chars().count() } } diff --git a/src/editor/tests/wide.rs b/src/editor/tests/wide.rs index 9fc1b0e..22ada8e 100644 --- a/src/editor/tests/wide.rs +++ b/src/editor/tests/wide.rs @@ -6,7 +6,7 @@ use super::editor; #[gpui::test] fn a_wide_character_moves_the_caret_by_its_width(cx: &mut TestAppContext) { - // The test text system draws "😄" two ems wide, "a" one. + // Test text draws "😄" two ems wide, "a" one. let (editor, cx) = editor("😄a", cx); cx.run_until_parked(); let x = |utf16: usize, cx: &mut VisualTestContext| { @@ -24,3 +24,42 @@ fn a_wide_character_moves_the_caret_by_its_width(cx: &mut TestAppContext) { }); assert_eq!(at, Some("😄".len()), "a press near its end lands after it"); } + +#[gpui::test] +fn a_press_on_the_last_character_takes_its_nearer_edge(cx: &mut TestAppContext) { + for (text, last) in [("ab", "a".len()), ("测", 0)] { + let (editor, cx) = editor(text, cx); + cx.run_until_parked(); + let at = |share: f32, cx: &mut VisualTestContext| { + editor.update_in(cx, |editor, window, cx| { + let mut x = |utf16: usize| { + let bounds = + editor.bounds_for_range(utf16..utf16, Bounds::default(), window, cx); + bounds.expect("a drawn line").origin.x + }; + let (start, from, to) = (x(0), x(last), x(text.encode_utf16().count())); + let left = editor.metrics.left; + editor.offset_in(0, left + from - start + (to - from) * share, window, cx) + }) + }; + assert_eq!(at(0.1, cx), Some(last), "{text}: near its start"); + assert_eq!(at(0.9, cx), Some(text.len()), "{text}: near its end"); + } +} + +#[gpui::test] +fn an_input_method_edit_measures_before_the_next_draw(cx: &mut TestAppContext) { + let (editor, cx) = editor("(a)", cx); + cx.run_until_parked(); + editor.update_in(cx, |editor, window, cx| { + editor.select([0..0], cx); + editor.replace_and_mark_text_in_range(None, "测", None, window, cx); + let text = editor.row_text(0).to_string(); + for (range, _) in editor.styles(0, cx) { + let whole = text.is_char_boundary(range.start) && text.is_char_boundary(range.end); + assert!(whole, "{range:?} splits a character of {text}"); + } + let bounds = editor.bounds_for_range(1..1, Bounds::default(), window, cx); + assert!(bounds.is_some(), "the caret after it is measured"); + }); +} diff --git a/src/editor/view.rs b/src/editor/view.rs index 6af61dd..75e79c2 100644 --- a/src/editor/view.rs +++ b/src/editor/view.rs @@ -25,6 +25,8 @@ pub(crate) struct Frame { pub line: Pixels, pub matched: Option<(usize, usize)>, pub occurrences: Vec>, + /// The buffer stamp these offsets were found in. + pub stamp: u64, /// The minimap's box, from its last paint. pub minimap: Bounds, } @@ -267,6 +269,7 @@ impl Render for CodeEditor { line, matched, occurrences, + stamp: self.buffer.stamp(), minimap, }; self.metrics.advance = advance;