From a643eb2ecfef387215a4e9c850ab7285608e9d83 Mon Sep 17 00:00:00 2001 From: jamie futch Date: Fri, 12 Dec 2025 19:54:24 -0500 Subject: [PATCH] added virtual space --- crates/edit/src/bin/edit/draw_menubar.rs | 6 ++ crates/edit/src/buffer/mod.rs | 78 +++++++++++++++++++++--- crates/edit/src/unicode/measurement.rs | 35 +++++++++++ i18n/edit.toml | 3 + 4 files changed, 115 insertions(+), 7 deletions(-) diff --git a/crates/edit/src/bin/edit/draw_menubar.rs b/crates/edit/src/bin/edit/draw_menubar.rs index 8f14f70c1ed0..316e0677ac5d 100644 --- a/crates/edit/src/bin/edit/draw_menubar.rs +++ b/crates/edit/src/bin/edit/draw_menubar.rs @@ -121,6 +121,12 @@ fn draw_menu_view(ctx: &mut Context, state: &mut State) { tb.set_word_wrap(!word_wrap); ctx.needs_rerender(); } + + let virtual_space = tb.is_virtual_space_enabled(); + if ctx.menubar_menu_checkbox(loc(LocId::ViewVirtualSpace), 'V', vk::NULL, virtual_space) { + tb.set_virtual_space_enabled(!virtual_space); + ctx.needs_rerender(); + } } ctx.menubar_menu_end(); diff --git a/crates/edit/src/buffer/mod.rs b/crates/edit/src/buffer/mod.rs index 9ea448542f6f..15301b5923ec 100644 --- a/crates/edit/src/buffer/mod.rs +++ b/crates/edit/src/buffer/mod.rs @@ -245,6 +245,7 @@ pub struct TextBuffer { overtype: bool, wants_cursor_visibility: bool, + virtual_space_enabled: bool, } impl TextBuffer { @@ -293,6 +294,7 @@ impl TextBuffer { overtype: false, wants_cursor_visibility: false, + virtual_space_enabled: false, }) } @@ -442,6 +444,18 @@ impl TextBuffer { self.overtype = overtype; } + /// Whether virtual space is enabled. + pub fn is_virtual_space_enabled(&self) -> bool { + self.virtual_space_enabled + } + + /// Enable or disable virtual space. + pub fn set_virtual_space_enabled(&mut self, enabled: bool) { + self.virtual_space_enabled = enabled; + // If disabling, we should probably snap cursor back to valid range? + // But for now let's just keep it simple. If user moves, it will snap. + } + /// Gets the logical cursor position, that is, /// the position in lines and graphemes per line. pub fn cursor_logical_pos(&self) -> Point { @@ -451,7 +465,11 @@ impl TextBuffer { /// Gets the visual cursor position, that is, /// the position in laid out rows and columns. pub fn cursor_visual_pos(&self) -> Point { - self.cursor.visual_pos + let mut pos = self.cursor.visual_pos; + if self.virtual_space_enabled { + pos.x += self.cursor.virtual_off; + } + pos } /// Gets the width of the left margin. @@ -1567,7 +1585,15 @@ impl TextBuffer { } } - self.measurement_config().with_cursor(cursor).goto_visual(pos) + let mut cursor = self.measurement_config().with_cursor(cursor).goto_visual(pos); + + if self.virtual_space_enabled && pos.y == cursor.visual_pos.y && pos.x > cursor.visual_pos.x { + cursor.virtual_off = pos.x - cursor.visual_pos.x; + } else { + cursor.virtual_off = 0; + } + + cursor } fn cursor_move_delta_internal( @@ -1582,11 +1608,25 @@ impl TextBuffer { let sign = if delta > 0 { 1 } else { -1 }; - match granularity { - CursorMovement::Grapheme => { - let start_x = if delta > 0 { 0 } else { CoordType::MAX }; + match granularity { + CursorMovement::Grapheme => { + if self.virtual_space_enabled && cursor.virtual_off > 0 { + if delta > 0 { + cursor.virtual_off = cursor.virtual_off.saturating_add(delta); + return cursor; + } else { + let remove = (-delta).min(cursor.virtual_off); + cursor.virtual_off -= remove; + delta += remove; + if delta == 0 { + return cursor; + } + } + } + + let start_x = if delta > 0 { 0 } else { CoordType::MAX }; - loop { + loop { let target_x = cursor.logical_pos.x + delta; cursor = self.cursor_move_to_logical_internal( @@ -2001,6 +2041,10 @@ impl TextBuffer { y += 1; } + if self.virtual_space_enabled { + x += self.cursor.virtual_off; + } + // Move the cursor into screen space. x += destination.left - origin.x + self.margin_width; y += destination.top - origin.y; @@ -2080,7 +2124,17 @@ impl TextBuffer { self.write(text, self.cursor, true); } - fn write(&mut self, text: &[u8], at: Cursor, raw: bool) { + fn write(&mut self, text: &[u8], mut at: Cursor, raw: bool) { + if self.virtual_space_enabled && at.virtual_off > 0 { + let mut new_text = Vec::with_capacity(at.virtual_off as usize + text.len()); + for _ in 0..at.virtual_off { + new_text.push(b' '); + } + new_text.extend_from_slice(text); + at.virtual_off = 0; + return self.write(&new_text, at, raw); + } + let history_type = if raw { HistoryType::Other } else { HistoryType::Write }; let mut edit_begun = false; @@ -2247,6 +2301,16 @@ impl TextBuffer { return; } + if self.virtual_space_enabled && delta < 0 && self.cursor.virtual_off > 0 && !self.has_selection() { + let remove = (-delta).min(self.cursor.virtual_off); + self.cursor.virtual_off -= remove; + let remaining = delta + remove; + if remaining == 0 { + return; + } + return self.delete(granularity, remaining); + } + let mut beg; let mut end; diff --git a/crates/edit/src/unicode/measurement.rs b/crates/edit/src/unicode/measurement.rs index 38e22adf6d45..d514ba3a0247 100644 --- a/crates/edit/src/unicode/measurement.rs +++ b/crates/edit/src/unicode/measurement.rs @@ -53,6 +53,9 @@ pub struct Cursor { /// a hard-wrap is required; otherwise, the word that is being laid-out is /// moved to the next line. This boolean carries this state between calls. pub wrap_opp: bool, + /// The number of visual columns the cursor is past the end of the line. + /// This is used for "virtual space". + pub virtual_off: CoordType, } /// Your entrypoint to navigating inside a [`ReadableDocument`]. @@ -464,6 +467,7 @@ impl<'doc> MeasurementConfig<'doc> { self.cursor.visual_pos = Point { x: visual_pos_x, y: visual_pos_y }; self.cursor.column = column; self.cursor.wrap_opp = wrap_opp; + self.cursor.virtual_off = 0; self.cursor } @@ -549,6 +553,7 @@ mod test { visual_pos: Point { x: 0, y: 1 }, column: 0, wrap_opp: false, + virtual_off: 0, } ); } @@ -564,6 +569,7 @@ mod test { visual_pos: Point { x: 1, y: 0 }, column: 1, wrap_opp: false, + virtual_off: 0, } ); } @@ -586,6 +592,7 @@ mod test { visual_pos: Point { x: 1, y: 1 }, column: 5, wrap_opp: true, + virtual_off: 0, } ); @@ -600,6 +607,7 @@ mod test { visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, + virtual_off: 0, } ); @@ -610,6 +618,7 @@ mod test { visual_pos: Point { x: 1, y: 0 }, column: 1, wrap_opp: false, + virtual_off: 0, }); let cursor = cfg.goto_visual(Point { x: 5, y: 0 }); assert_eq!( @@ -620,6 +629,7 @@ mod test { visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, + virtual_off: 0, } ); @@ -632,6 +642,7 @@ mod test { visual_pos: Point { x: 0, y: 1 }, column: 4, wrap_opp: false, + virtual_off: 0, } ); @@ -644,6 +655,7 @@ mod test { visual_pos: Point { x: 4, y: 1 }, column: 8, wrap_opp: false, + virtual_off: 0, } ); @@ -656,6 +668,7 @@ mod test { visual_pos: Point { x: 0, y: 2 }, column: 0, wrap_opp: false, + virtual_off: 0, } ); @@ -668,6 +681,7 @@ mod test { visual_pos: Point { x: 3, y: 2 }, column: 3, wrap_opp: false, + virtual_off: 0, } ); } @@ -685,6 +699,7 @@ mod test { visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: false, + virtual_off: 0, } ); } @@ -721,6 +736,7 @@ mod test { visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, + virtual_off: 0, } ); @@ -733,6 +749,7 @@ mod test { visual_pos: Point { x: 0, y: 1 }, column: 4, wrap_opp: false, + virtual_off: 0, } ); @@ -745,6 +762,7 @@ mod test { visual_pos: Point { x: 4, y: 1 }, column: 8, wrap_opp: false, + virtual_off: 0, } ); @@ -757,6 +775,7 @@ mod test { visual_pos: Point { x: 0, y: 2 }, column: 0, wrap_opp: false, + virtual_off: 0, } ); @@ -769,6 +788,7 @@ mod test { visual_pos: Point { x: 3, y: 2 }, column: 3, wrap_opp: false, + virtual_off: 0, } ); } @@ -792,6 +812,7 @@ mod test { visual_pos: Point { x: 3, y: 0 }, column: 3, wrap_opp: true, + virtual_off: 0, } ); @@ -805,6 +826,7 @@ mod test { visual_pos: Point { x: 0, y: 1 }, column: 3, wrap_opp: false, + virtual_off: 0, } ); @@ -821,6 +843,7 @@ mod test { visual_pos: Point { x: 1, y: 1 }, column: 4, wrap_opp: false, + virtual_off: 0, } ); @@ -834,6 +857,7 @@ mod test { visual_pos: Point { x: 8, y: 1 }, column: 11, wrap_opp: true, + virtual_off: 0, } ); @@ -847,6 +871,7 @@ mod test { visual_pos: Point { x: 4, y: 2 }, column: 15, wrap_opp: false, + virtual_off: 0, } ); } @@ -891,6 +916,7 @@ mod test { visual_pos: Point { x: 3, y: 0 }, column: 3, wrap_opp: true, + virtual_off: 0, } ); @@ -903,6 +929,7 @@ mod test { visual_pos: Point { x: 3, y: 1 }, column: 6, wrap_opp: false, + virtual_off: 0, } ); @@ -915,6 +942,7 @@ mod test { visual_pos: Point { x: 3, y: 2 }, column: 14, wrap_opp: false, + virtual_off: 0, } ); } @@ -936,6 +964,7 @@ mod test { visual_pos: Point { x: 8, y: 0 }, column: 8, wrap_opp: true, + virtual_off: 0, } ); @@ -948,6 +977,7 @@ mod test { visual_pos: Point { x: 7, y: 1 }, column: 15, wrap_opp: true, + virtual_off: 0, } ); } @@ -989,6 +1019,7 @@ mod test { visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, + virtual_off: 0, }, ); @@ -1001,6 +1032,7 @@ mod test { visual_pos: Point { x: 0, y: 1 }, column: 4, wrap_opp: false, + virtual_off: 0, }, ); @@ -1013,6 +1045,7 @@ mod test { visual_pos: Point { x: 6, y: 1 }, column: 10, wrap_opp: true, + virtual_off: 0, }, ); } @@ -1029,6 +1062,7 @@ mod test { visual_pos: Point { x: 3, y: 1 }, column: 3, wrap_opp: false, + virtual_off: 0, } ); } @@ -1049,6 +1083,7 @@ mod test { visual_pos: Point { x: 2, y: 1 }, column: 8, wrap_opp: false, + virtual_off: 0, } ); } diff --git a/i18n/edit.toml b/i18n/edit.toml index 01248b1577ad..8c32282ac039 100644 --- a/i18n/edit.toml +++ b/i18n/edit.toml @@ -876,6 +876,9 @@ vi = "Ngắt dòng tự động" zh_hans = "自动换行" zh_hant = "自動換行" +[ViewVirtualSpace] +en = "Enable Virtual Space" + [ViewGoToFile] en = "Go to File…" ar = "الانتقال إلى ملف…"