From d42b199ccc29934cfad69f79db12ed070e8bd406 Mon Sep 17 00:00:00 2001 From: Noktoborus Date: Sun, 21 Jul 2024 23:00:14 +0400 Subject: [PATCH] add 'overtype' edit mode --- helix-core/src/transaction.rs | 7 +++++ helix-term/src/commands.rs | 45 ++++++++++++++++++++++++++++++-- helix-term/src/keymap/default.rs | 4 +++ helix-term/src/ui/editor.rs | 32 +++++++++++++++++++++++ helix-term/src/ui/statusline.rs | 2 ++ helix-view/src/document.rs | 3 +++ helix-view/src/editor.rs | 11 +++++--- 7 files changed, 98 insertions(+), 6 deletions(-) diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index f24f20942..4dcfa375a 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -727,6 +727,13 @@ impl Transaction { }) } + /// Replace text at each selection head. + pub fn replace(doc: &Rope, selection: &Selection, text: Tendril) -> Self { + Self::change_by_selection(doc, selection, |range| { + (range.from(), range.to(), Some(text.clone())) + }) + } + pub fn changes_iter(&self) -> ChangeIterator { self.changes.changes_iter() } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7e0bee92b..b3293f2d8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -291,6 +291,7 @@ impl MappableCommand { extend_prev_char, "Extend to previous occurrence of char", repeat_last_motion, "Repeat last motion", replace, "Replace with new char", + overtype_mode, "Enter overtype mode", switch_case, "Switch (toggle) case", switch_to_uppercase, "Switch to uppercase", switch_to_lowercase, "Switch to lowercase", @@ -2788,6 +2789,29 @@ fn insert_mode(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn enter_overtype_mode(cx: &mut Context) { + cx.editor.mode = Mode::Overtype; +} + +// inserts at the start of each selection +fn overtype_mode(cx: &mut Context) { + enter_overtype_mode(cx); + let (view, doc) = current!(cx.editor); + + log::trace!( + "entering replace mode with sel: {:?}, text: {:?}", + doc.selection(view.id), + doc.text().to_string() + ); + + let selection = doc + .selection(view.id) + .clone() + .transform(|range| Range::new(range.to(), range.from())); + + doc.set_selection(view.id, selection); +} + // inserts at the end of each selection fn append_mode(cx: &mut Context) { enter_insert_mode(cx); @@ -3746,7 +3770,7 @@ pub mod insert { Some(transaction) } - use helix_core::auto_pairs; + use helix_core::{auto_pairs, graphemes::nth_next_grapheme_boundary}; use helix_view::editor::SmartTabConfig; pub fn insert_char(cx: &mut Context, c: char) { @@ -3768,6 +3792,23 @@ pub mod insert { helix_event::dispatch(PostInsertChar { c, cx }); } + pub fn replace_char(cx: &mut Context, c: char) { + let (view, doc) = current!(cx.editor); + let text = doc.text(); + let selection = doc.selection(view.id); + let slice = text.slice(..); + let selection_update = selection.clone().transform(|range| { + let new_pos = nth_next_grapheme_boundary(slice, range.cursor(slice), 1); + range.put_cursor(slice, new_pos, false) + }); + let mut t = Tendril::new(); + t.push(c); + let transaction = Transaction::replace(text, &selection, t); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view); + doc.set_selection(view.id, selection_update); + } + pub fn smart_tab(cx: &mut Context) { let (view, doc) = current_ref!(cx.editor); let view_id = view.id; @@ -4276,7 +4317,7 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) { let count = cx.count(); let paste = match cx.editor.mode { Mode::Insert | Mode::Select => Paste::Cursor, - Mode::Normal => Paste::Before, + Mode::Normal | Mode::Overtype => Paste::Before, }; let (view, doc) = current!(cx.editor); paste_impl(&[contents], doc, view, paste, count, cx.editor.mode); diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5a3e8eed4..343a75e77 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -395,9 +395,13 @@ pub fn default() -> HashMap { "home" => goto_line_start, "end" => goto_line_end_newline, }); + let overtype = keymap!({ "Overtype mode" + "esc" => normal_mode, + }); hashmap!( Mode::Normal => normal, Mode::Select => select, Mode::Insert => insert, + Mode::Overtype => overtype, ) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index c151a7dd5..512aa2e3c 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -488,6 +488,7 @@ impl EditorView { let cursor_scope = match mode { Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"), + Mode::Overtype => theme.find_scope_index_exact("ui.cursor.overtype"), Mode::Select => theme.find_scope_index_exact("ui.cursor.select"), Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"), } @@ -495,6 +496,7 @@ impl EditorView { let primary_cursor_scope = match mode { Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"), + Mode::Overtype => theme.find_scope_index_exact("ui.cursor.overtype"), Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"), Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"), } @@ -908,6 +910,33 @@ impl EditorView { None } + fn overtype_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { + if let Some(keyresult) = self.handle_keymap_event(Mode::Overtype, cx, event) { + match keyresult { + KeymapResult::NotFound => { + if let Some(ch) = event.char() { + commands::insert::replace_char(cx, ch) + } + } + KeymapResult::Cancelled(pending) => { + for ev in pending { + match ev.char() { + Some(ch) => commands::insert::replace_char(cx, ch), + None => { + if let KeymapResult::Matched(command) = + self.keymaps.get(Mode::Overtype, ev) + { + command.execute(cx); + } + } + } + } + } + _ => unreachable!(), + } + } + } + fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { match keyresult { @@ -1406,6 +1435,9 @@ impl Component for EditorView { self.last_insert.1.push(InsertEvent::Key(key)); } } + Mode::Overtype => { + self.overtype_mode(&mut cx, key); + } mode => self.command_mode(mode, &mut cx, key), } } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd07..b722832f0 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -180,6 +180,7 @@ where if visible { match context.editor.mode() { Mode::Insert => &modenames.insert, + Mode::Overtype => &modenames.overtype, Mode::Select => &modenames.select, Mode::Normal => &modenames.normal, } @@ -191,6 +192,7 @@ where if visible && config.color_modes { match context.editor.mode() { Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")), + Mode::Overtype => Some(context.editor.theme.get("ui.statusline.overtype")), Mode::Select => Some(context.editor.theme.get("ui.statusline.select")), Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")), } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f3ace89e5..d2fdee9c0 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -55,6 +55,7 @@ pub enum Mode { Normal = 0, Select = 1, Insert = 2, + Overtype = 3, } impl Display for Mode { @@ -63,6 +64,7 @@ impl Display for Mode { Mode::Normal => f.write_str("normal"), Mode::Select => f.write_str("select"), Mode::Insert => f.write_str("insert"), + Mode::Overtype => f.write_str("overtype"), } } } @@ -75,6 +77,7 @@ impl FromStr for Mode { "normal" => Ok(Mode::Normal), "select" => Ok(Mode::Select), "insert" => Ok(Mode::Insert), + "overtype" => Ok(Mode::Overtype), _ => bail!("Invalid mode '{}'", s), } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cead30d7c..8cc9c206b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -501,6 +501,7 @@ impl Default for StatusLineConfig { pub struct ModeConfig { pub normal: String, pub insert: String, + pub overtype: String, pub select: String, } @@ -509,6 +510,7 @@ impl Default for ModeConfig { Self { normal: String::from("NOR"), insert: String::from("INS"), + overtype: String::from("REP"), select: String::from("SEL"), } } @@ -584,7 +586,7 @@ pub enum StatusLineElement { // Cursor shape is read and used on every rendered frame and so needs // to be fast. Therefore we avoid a hashmap and use an enum indexed array. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct CursorShapeConfig([CursorKind; 3]); +pub struct CursorShapeConfig([CursorKind; 4]); impl CursorShapeConfig { pub fn from_mode(&self, mode: Mode) -> CursorKind { @@ -603,6 +605,7 @@ impl<'de> Deserialize<'de> for CursorShapeConfig { into_cursor(Mode::Normal), into_cursor(Mode::Select), into_cursor(Mode::Insert), + into_cursor(Mode::Overtype), ])) } } @@ -613,7 +616,7 @@ impl Serialize for CursorShapeConfig { S: serde::Serializer, { let mut map = serializer.serialize_map(Some(self.len()))?; - let modes = [Mode::Normal, Mode::Select, Mode::Insert]; + let modes = [Mode::Normal, Mode::Select, Mode::Insert, Mode::Overtype]; for mode in modes { map.serialize_entry(&mode, &self.from_mode(mode))?; } @@ -622,7 +625,7 @@ impl Serialize for CursorShapeConfig { } impl std::ops::Deref for CursorShapeConfig { - type Target = [CursorKind; 3]; + type Target = [CursorKind; 4]; fn deref(&self) -> &Self::Target { &self.0 @@ -631,7 +634,7 @@ impl std::ops::Deref for CursorShapeConfig { impl Default for CursorShapeConfig { fn default() -> Self { - Self([CursorKind::Block; 3]) + Self([CursorKind::Block; 4]) } }