diff --git a/local/recipes/tui/tlc/source/src/editor/dispatch.rs b/local/recipes/tui/tlc/source/src/editor/dispatch.rs new file mode 100644 index 0000000000..7d6499e362 --- /dev/null +++ b/local/recipes/tui/tlc/source/src/editor/dispatch.rs @@ -0,0 +1,331 @@ +//! Editor command dispatch. +//! +//! `dispatch_editor_cmd` handles [`EditorCmd`] variants from the +//! F9 menubar, routing each command to the appropriate editor method. +//! Originally part of the monolithic `editor/mod.rs`; extracted here +//! to keep the Editor struct definition and constructors separate +//! from the dispatch logic. + +use crate::editor::menubar::EditorCmd; +use crate::editor::{Editor, EditorResult, Mode, PromptKind}; + +impl Editor { + /// Dispatch a menu-bar [`EditorCmd`] to the corresponding editor + /// method. Returns the resulting [`EditorResult`]. Called by the + /// F9 menubar handler. + pub(crate) fn dispatch_editor_cmd(&mut self, cmd: EditorCmd) -> EditorResult { + match cmd { + EditorCmd::Undo => { + self.undo(); + } + EditorCmd::Redo => { + self.redo(); + } + EditorCmd::WordComplete => { + self.word_complete(); + } + EditorCmd::SaveSettings => { + self.message = Some("Save via F9→Options→Save setup".to_string()); + } + EditorCmd::Refresh => { + // TLC redraws every frame; CK_Refresh is a no-op. + } + EditorCmd::SpellCheck => { + let misspelled = self.find_next_misspelled(self.cursor.position()); + if let Some((start, end)) = misspelled { + self.cursor.set_position(end, &self.buffer); + self.cursor.start_selection(); + self.cursor.set_position(start, &self.buffer); + self.message = + Some("Misspelled word — press Alt-Tab for suggestions".to_string()); + } else { + self.message = Some("No more misspelled words".to_string()); + } + } + EditorCmd::Cut => { + if let Some(text) = self.cursor.selected_text(&self.buffer) { + self.clipboard = Some(text.clone()); + let _ = crate::editor::clipboard_osc52::osc52_copy(&text); + self.cursor.delete_selection(&mut self.buffer); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = self.buffer.is_modified(); + self.message = Some("Block cut".to_string()); + } + } + EditorCmd::Copy => { + if let Some(text) = self.cursor.selected_text(&self.buffer) { + self.clipboard = Some(text.clone()); + let _ = crate::editor::clipboard_osc52::osc52_copy(&text); + self.message = Some("Block copied".to_string()); + } + } + EditorCmd::ToggleColumnMode => { + if self.cursor.selection_mode() + == crate::editor::cursor::SelectionMode::Column + { + self.cursor.clear_selection(); + self.message = Some("Stream selection".to_string()); + } else { + self.cursor.start_column_selection(); + self.message = Some("Column selection".to_string()); + } + } + EditorCmd::Delete => { + if self.cursor.has_selection() { + self.cursor.delete_selection(&mut self.buffer); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = self.buffer.is_modified(); + self.message = Some("Deleted".to_string()); + } else { + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + let _ = self.cursor.select_right(&mut self.buffer); + if self.cursor.has_selection() { + self.cursor.delete_selection(&mut self.buffer); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = self.buffer.is_modified(); + } + } + } + EditorCmd::Paste => { + if let Some(text) = self.clipboard.clone() { + if self.cursor.has_selection() { + self.cursor.delete_selection(&mut self.buffer); + } + self.insert_str(&text); + self.message = Some("Pasted".to_string()); + } + } + EditorCmd::GotoTop => { + self.cursor.set_position(0, &self.buffer); + } + EditorCmd::GotoBottom => { + self.cursor.set_position(self.buffer.len(), &self.buffer); + } + EditorCmd::BookmarkToggle => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::BookmarkSet); + } + EditorCmd::BookmarkClearAll => { + let names = self.bookmarks.names(); + let count = names.len(); + for n in names { + self.bookmarks.clear(n); + } + self.message = Some(format!("{count} bookmarks cleared")); + } + EditorCmd::GotoLine => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::GotoLine); + } + EditorCmd::ToggleAutoIndent => { + let on = self.toggle_auto_indent(); + self.message = Some(if on { + "Auto-indent: ON".to_string() + } else { + "Auto-indent: OFF".to_string() + }); + } + EditorCmd::FormatParagraph => { + self.format_paragraph(); + } + EditorCmd::ToggleShowWhitespace => { + let on = self.toggle_show_whitespace(); + self.message = Some(if on { + "Show whitespace: ON".to_string() + } else { + "Show whitespace: OFF".to_string() + }); + } + EditorCmd::ToggleWordWrap => { + let on = self.toggle_word_wrap(); + self.message = Some(if on { + "Word wrap: ON".to_string() + } else { + "Word wrap: OFF".to_string() + }); + } + EditorCmd::ToggleSyntax => { + self.syntax_enabled = !self.syntax_enabled; + self.message = Some(if self.syntax_enabled { + "Syntax: ON".to_string() + } else { + "Syntax: OFF".to_string() + }); + } + EditorCmd::Settings => { + let ai = if self.auto_indent { "ON" } else { "OFF" }; + let ww = if self.word_wrap { "ON" } else { "OFF" }; + let ws = if self.show_whitespace { "ON" } else { "OFF" }; + let sy = if self.syntax_enabled { "ON" } else { "OFF" }; + self.message = Some(format!( + "Settings: auto-indent={ai} wrap={ww} whitespace={ws} syntax={sy} (Alt-A/Alt-W/Alt-E/Ctrl-S toggle)" + )); + } + EditorCmd::New => { + let mut new_editor = Self::new_empty(); + std::mem::swap(&mut self.buffer, &mut new_editor.buffer); + std::mem::swap(&mut self.cursor, &mut new_editor.cursor); + self.path = None; + self.modified = false; + self.title = "*new*".to_string(); + self.message = Some("New buffer".to_string()); + } + EditorCmd::Open => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::InsertFile); + } + EditorCmd::Save => match self.save() { + Ok(()) => self.message = Some("Saved".to_string()), + Err(e) => self.message = Some(format!("Save error: {e}")), + }, + EditorCmd::SaveAs => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::SaveAs); + } + EditorCmd::Quit => { + if self.modified { + self.open_save_before_close_prompt(); + } else { + return EditorResult::Close; + } + } + EditorCmd::Find => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::Find); + } + EditorCmd::FindNext => { + let from = self.cursor.position(); + if let Some(m) = self.search.find_next(&self.buffer, from) { + self.buffer.set_cursor(m.range.start); + self.cursor.set_position(m.range.start, &self.buffer); + self.message = Some(if self.search.last_wrapped() { + "Search wrapped".to_string() + } else { + "Match found".to_string() + }); + } else { + self.message = Some("No more matches".to_string()); + } + } + EditorCmd::FindPrev => { + let from = self.cursor.position(); + if let Some(m) = self.search.find_prev(&self.buffer, from) { + self.buffer.set_cursor(m.range.start); + self.cursor.set_position(m.range.start, &self.buffer); + self.message = Some(if self.search.last_wrapped() { + "Search wrapped".to_string() + } else { + "Match found".to_string() + }); + } else { + self.message = Some("No more matches".to_string()); + } + } + EditorCmd::Replace => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::Replace); + } + EditorCmd::BookmarkNext => { + let cur_line = self.buffer.line_of_cursor() as u32; + let all: Vec<(char, crate::editor::bookmark::Mark)> = self + .bookmarks + .names() + .into_iter() + .filter_map(|n| self.bookmarks.get(n).map(|m| (n, m))) + .collect(); + let target = all + .iter() + .find(|(_, m)| m.line > cur_line) + .or_else(|| all.first()); + if let Some((name, mark)) = target { + if let Ok(off) = crate::editor::goto::col_to_offset( + &self.buffer, + mark.line + 1, + mark.col + 1, + ) { + self.buffer.set_cursor(off); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.message = Some(format!("Bookmark '{}'", name)); + } + } else { + self.message = Some("No bookmarks set".to_string()); + } + } + EditorCmd::BookmarkPrev => { + let cur_line = self.buffer.line_of_cursor() as u32; + let mut all: Vec<(char, crate::editor::bookmark::Mark)> = self + .bookmarks + .names() + .into_iter() + .filter_map(|n| self.bookmarks.get(n).map(|m| (n, m))) + .collect(); + all.sort_by_key(|(_, m)| m.line); + let target = all + .iter() + .rev() + .find(|(_, m)| m.line < cur_line) + .or_else(|| all.last()); + if let Some((name, mark)) = target { + if let Ok(off) = crate::editor::goto::col_to_offset( + &self.buffer, + mark.line + 1, + mark.col + 1, + ) { + self.buffer.set_cursor(off); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.message = Some(format!("Bookmark '{}'", name)); + } + } else { + self.message = Some("No bookmarks set".to_string()); + } + } + EditorCmd::EditUserMenu => { + let menu = crate::filemanager::usermenu::UserMenu::new(); + let path = menu.storage_path.clone(); + let new_editor = Self::open(&path); + *self = new_editor; + self.message = Some(format!("Editing user menu: {}", path.display())); + } + EditorCmd::MarkAll => { + let len = self.buffer.len(); + self.cursor.set_position(0, &self.buffer); + self.cursor.start_selection(); + self.cursor.set_position(len, &self.buffer); + } + EditorCmd::InsertFile => { + self.prompt_input.clear(); + self.mode = Mode::Prompt(PromptKind::InsertFile); + } + EditorCmd::BlockSave => { + if let Some(text) = self.cursor.selected_text(&self.buffer) { + self.prompt_input.clear(); + self.prompt_input.text = text; + self.prompt_input.cursor = + self.prompt_input.text.chars().count(); + self.mode = Mode::Prompt(PromptKind::SaveAs); + } else { + self.message = Some("No selection".to_string()); + } + } + EditorCmd::ToggleOverwrite => { + self.overwrite = !self.overwrite; + self.message = Some(if self.overwrite { + "[OVR]".to_string() + } else { + "[INS]".to_string() + }); + } + EditorCmd::Close => { + if self.modified { + self.mode = Mode::Prompt(PromptKind::SaveBeforeClose); + } else { + *self = Self::new_empty(); + } + } + _ => { + self.message = Some(format!("F9: {:?} (not yet wired)", cmd)); + } + } + EditorResult::Running + } +} diff --git a/local/recipes/tui/tlc/source/src/editor/edit.rs b/local/recipes/tui/tlc/source/src/editor/edit.rs new file mode 100644 index 0000000000..324917f035 --- /dev/null +++ b/local/recipes/tui/tlc/source/src/editor/edit.rs @@ -0,0 +1,214 @@ +//! Text editing operations. +//! +//! Low-level character insertion, deletion, and multi-cursor editing +//! methods. These are the primitive operations that every key handler +//! and menu command delegates to. Extracted from the monolithic +//! `editor/mod.rs` to keep the Editor struct and its editing surface +//! separate. + +use crate::editor::Editor; + +impl Editor { + /// Insert a character at the cursor. Marks the buffer as modified. + pub fn insert_char(&mut self, c: char) { + self.completer.cancel(); + let line_before = self.buffer_line_of(self.buffer.cursor()); + let lines_before = self.buffer.line_count(); + self.buffer.insert_char(c); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = true; + } + + /// Insert a string at the cursor. + pub fn insert_str(&mut self, s: &str) { + let line_before = self.buffer_line_of(self.buffer.cursor()); + let lines_before = self.buffer.line_count(); + self.buffer.insert_str(s); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = true; + } + + /// Recompute bookmark line numbers after a buffer edit. + fn adjust_bookmarks_after_edit(&mut self, edit_line: usize, lines_before: usize) { + let lines_after = self.buffer.line_count(); + let delta = lines_after as i64 - lines_before as i64; + if delta != 0 { + self.bookmarks.adjust_lines(edit_line as u32, delta as i32); + } + } + + /// Insert a Tab with indent-aware behavior (MC parity). + /// + /// When the cursor is positioned at or before the first + /// non-whitespace character on its line, insert enough spaces to + /// advance to the next `tab_width` boundary. Otherwise insert a + /// single literal `\t`. + pub fn insert_tab_with_indent(&mut self) { + self.completer.cancel(); + let col = self.cursor.visual_column(); + let next_boundary = ((col / self.tab_width) + 1) * self.tab_width; + if self.is_in_indent_zone() { + let spaces = next_boundary.saturating_sub(col).max(1); + self.buffer.insert_str(&" ".repeat(spaces)); + } else { + self.buffer.insert_char('\t'); + } + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = true; + } + + /// True if the cursor is on a line where all characters before the + /// cursor are whitespace. + fn is_in_indent_zone(&self) -> bool { + let cursor_byte = self.cursor.position(); + let line_idx = self.buffer_line_of(cursor_byte); + let line_start = self.buffer.line_offset(line_idx); + if cursor_byte <= line_start { + return true; + } + let bytes = self.buffer.to_bytes(); + let prefix = bytes.get(line_start..cursor_byte).unwrap_or(&[]); + prefix.iter().all(|&b| b == b' ' || b == b'\t') + } + + /// Backspace at the cursor. + pub fn delete_back(&mut self) { + let line_before = self.buffer_line_of(self.buffer.cursor()); + let lines_before = self.buffer.line_count(); + self.buffer.delete_back(); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = true; + } + + /// Delete at the cursor. + pub fn delete_forward(&mut self) { + let line_before = self.buffer_line_of(self.buffer.cursor()); + let lines_before = self.buffer.line_count(); + self.buffer.delete_forward(); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.modified = true; + } + + /// Whether any secondary cursors are active. + pub fn has_multi_cursor(&self) -> bool { + !self.secondary_cursors.is_empty() + } + + /// Number of active secondary cursors. + pub fn secondary_cursor_count(&self) -> usize { + self.secondary_cursors.len() + } + + /// Add a secondary cursor at `byte_pos` (deduped, sorted). + pub fn add_secondary_cursor(&mut self, byte_pos: usize) { + let pos = byte_pos.min(self.buffer.len()); + if pos == self.cursor.position() { + return; + } + if !self.secondary_cursors.contains(&pos) { + self.secondary_cursors.push(pos); + self.secondary_cursors.sort_unstable(); + self.secondary_cursors.dedup(); + } + } + + /// Remove all secondary cursors. + pub fn clear_secondary_cursors(&mut self) { + self.secondary_cursors.clear(); + } + + /// Return all cursor positions (primary + secondary), sorted and + /// deduped. + pub fn all_cursor_positions(&self) -> Vec { + let mut all = self.secondary_cursors.clone(); + all.push(self.cursor.position()); + all.sort_unstable(); + all.dedup(); + all + } + + /// Insert `c` at every cursor position (primary + all secondary). + /// Positions are processed right-to-left; after insertion all + /// secondary cursor byte offsets are advanced by the char's UTF-8 + /// length. + pub fn insert_char_multi(&mut self, c: char) { + if self.secondary_cursors.is_empty() { + self.insert_char(c); + return; + } + self.completer.cancel(); + let char_len = c.len_utf8(); + let line_before = self.buffer_line_of(self.cursor.position()); + let lines_before = self.buffer.line_count(); + let positions = self.all_cursor_positions(); + for &pos in positions.iter().rev() { + self.buffer.set_cursor(pos); + self.buffer.insert_char(c); + } + for sec in &mut self.secondary_cursors { + *sec += char_len; + } + self.cursor + .set_position(self.cursor.position() + char_len, &self.buffer); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.modified = true; + } + + /// Backspace at every cursor position. Positions are processed + /// right-to-left. Secondary cursors at byte offset 0 are skipped. + pub fn delete_back_multi(&mut self) { + if self.secondary_cursors.is_empty() { + self.delete_back(); + return; + } + self.completer.cancel(); + let line_before = self.buffer_line_of(self.cursor.position()); + let lines_before = self.buffer.line_count(); + let positions = self.all_cursor_positions(); + for &pos in positions.iter().rev() { + if pos == 0 { + continue; + } + self.buffer.set_cursor(pos); + self.buffer.delete_back(); + } + let new_len = self.buffer.len(); + self.secondary_cursors.retain_mut(|p| { + if *p > 0 { + *p -= 1; + } + *p <= new_len + }); + let primary = self.cursor.position().saturating_sub(1); + self.cursor.set_position(primary, &self.buffer); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.modified = true; + } + + /// Delete-forward at every cursor position. Positions are processed + /// right-to-left. + pub fn delete_forward_multi(&mut self) { + if self.secondary_cursors.is_empty() { + self.delete_forward(); + return; + } + self.completer.cancel(); + let line_before = self.buffer_line_of(self.cursor.position()); + let lines_before = self.buffer.line_count(); + let positions = self.all_cursor_positions(); + for &pos in positions.iter().rev() { + self.buffer.set_cursor(pos); + self.buffer.delete_forward(); + } + let new_len = self.buffer.len(); + self.secondary_cursors.retain(|p| *p <= new_len); + self.cursor + .set_position(self.cursor.position().min(new_len), &self.buffer); + self.adjust_bookmarks_after_edit(line_before, lines_before); + self.modified = true; + } +} diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 6f0a14b20e..effe04a95a 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -40,6 +40,9 @@ pub mod completion; pub mod cursor; pub mod cursor_shape; /// Per-file cursor position save/restore (MC `~/.mc/filepos`). +pub mod dispatch; +pub mod edit; +/// Per-file cursor position save/restore (MC `~/.mc/filepos`). pub mod filepos; pub mod folding; pub mod format; @@ -1046,217 +1049,6 @@ impl Editor { Ok(()) } - /// Insert a character at the cursor. Marks the buffer as - /// modified. - pub fn insert_char(&mut self, c: char) { - self.completer.cancel(); - let line_before = self.buffer_line_of(self.buffer.cursor()); - let lines_before = self.buffer.line_count(); - self.buffer.insert_char(c); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = true; - } - - /// Insert a string at the cursor. - pub fn insert_str(&mut self, s: &str) { - let line_before = self.buffer_line_of(self.buffer.cursor()); - let lines_before = self.buffer.line_count(); - self.buffer.insert_str(s); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = true; - } - - /// Recompute bookmark line numbers after a buffer edit. Called by - /// every insert/delete entry point on `Editor`. `edit_line` is the - /// line where the edit happened; `lines_before` is the line count - /// before the edit. - fn adjust_bookmarks_after_edit(&mut self, edit_line: usize, lines_before: usize) { - let lines_after = self.buffer.line_count(); - let delta = lines_after as i64 - lines_before as i64; - if delta != 0 { - self.bookmarks.adjust_lines(edit_line as u32, delta as i32); - } - } - - /// Insert a Tab with indent-aware behavior (MC parity). - /// - /// When the cursor is positioned at or before the first - /// non-whitespace character on its line, insert enough spaces to - /// advance to the next `tab_width` boundary (the indent context). - /// Otherwise (cursor is mid-line or past all whitespace) insert - /// a single literal `\t` character (the column-alignment - /// context). - /// - /// The cursor's `visual_column` is tab-expanded; we use it to - /// decide which context applies. - pub fn insert_tab_with_indent(&mut self) { - self.completer.cancel(); - let col = self.cursor.visual_column(); - let next_boundary = ((col / self.tab_width) + 1) * self.tab_width; - if self.is_in_indent_zone() { - let spaces = next_boundary.saturating_sub(col).max(1); - self.buffer.insert_str(&" ".repeat(spaces)); - } else { - self.buffer.insert_char('\t'); - } - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = true; - } - - /// True if the cursor is on a line where all characters before - /// the cursor are whitespace. Used to decide between Tab-as-indent - /// and Tab-as-column-align. - fn is_in_indent_zone(&self) -> bool { - let cursor_byte = self.cursor.position(); - let line_idx = self.buffer_line_of(cursor_byte); - let line_start = self.buffer.line_offset(line_idx); - if cursor_byte <= line_start { - return true; - } - let bytes = self.buffer.to_bytes(); - let prefix = bytes.get(line_start..cursor_byte).unwrap_or(&[]); - prefix.iter().all(|&b| b == b' ' || b == b'\t') - } - - /// Backspace at the cursor. - pub fn delete_back(&mut self) { - let line_before = self.buffer_line_of(self.buffer.cursor()); - let lines_before = self.buffer.line_count(); - self.buffer.delete_back(); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = true; - } - - /// Delete at the cursor. - pub fn delete_forward(&mut self) { - let line_before = self.buffer_line_of(self.buffer.cursor()); - let lines_before = self.buffer.line_count(); - self.buffer.delete_forward(); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = true; - } - - /// Whether any secondary cursors are active. - pub fn has_multi_cursor(&self) -> bool { - !self.secondary_cursors.is_empty() - } - - /// Number of active secondary cursors. - pub fn secondary_cursor_count(&self) -> usize { - self.secondary_cursors.len() - } - - /// Add a secondary cursor at `byte_pos` (deduped, sorted). - pub fn add_secondary_cursor(&mut self, byte_pos: usize) { - let pos = byte_pos.min(self.buffer.len()); - if pos == self.cursor.position() { - return; - } - if !self.secondary_cursors.contains(&pos) { - self.secondary_cursors.push(pos); - self.secondary_cursors.sort_unstable(); - self.secondary_cursors.dedup(); - } - } - - /// Remove all secondary cursors. - pub fn clear_secondary_cursors(&mut self) { - self.secondary_cursors.clear(); - } - - /// Return all cursor positions (primary + secondary), sorted and deduped. - pub fn all_cursor_positions(&self) -> Vec { - let mut all = self.secondary_cursors.clone(); - all.push(self.cursor.position()); - all.sort_unstable(); - all.dedup(); - all - } - - /// Insert `c` at every cursor position (primary + all secondary). - /// Positions are processed right-to-left so earlier insertions - /// don't shift later positions. After insertion, all secondary - /// cursor byte offsets are advanced by the char's UTF-8 length. - pub fn insert_char_multi(&mut self, c: char) { - if self.secondary_cursors.is_empty() { - self.insert_char(c); - return; - } - self.completer.cancel(); - let char_len = c.len_utf8(); - let line_before = self.buffer_line_of(self.cursor.position()); - let lines_before = self.buffer.line_count(); - let positions = self.all_cursor_positions(); - for &pos in positions.iter().rev() { - self.buffer.set_cursor(pos); - self.buffer.insert_char(c); - } - for sec in &mut self.secondary_cursors { - *sec += char_len; - } - self.cursor.set_position( - self.cursor.position() + char_len, - &self.buffer, - ); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.modified = true; - } - - /// Backspace at every cursor position. Positions are processed - /// right-to-left. Secondary cursors at byte offset 0 are skipped. - pub fn delete_back_multi(&mut self) { - if self.secondary_cursors.is_empty() { - self.delete_back(); - return; - } - self.completer.cancel(); - let line_before = self.buffer_line_of(self.cursor.position()); - let lines_before = self.buffer.line_count(); - let positions = self.all_cursor_positions(); - for &pos in positions.iter().rev() { - if pos == 0 { - continue; - } - self.buffer.set_cursor(pos); - self.buffer.delete_back(); - } - let new_len = self.buffer.len(); - self.secondary_cursors.retain_mut(|p| { - if *p > 0 { *p -= 1; } - *p <= new_len - }); - let primary = self.cursor.position().saturating_sub(1); - self.cursor.set_position(primary, &self.buffer); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.modified = true; - } - - /// Delete-forward at every cursor position. Positions are processed - /// right-to-left so earlier deletions don't shift later positions. - pub fn delete_forward_multi(&mut self) { - if self.secondary_cursors.is_empty() { - self.delete_forward(); - return; - } - self.completer.cancel(); - let line_before = self.buffer_line_of(self.cursor.position()); - let lines_before = self.buffer.line_count(); - let positions = self.all_cursor_positions(); - for &pos in positions.iter().rev() { - self.buffer.set_cursor(pos); - self.buffer.delete_forward(); - } - let new_len = self.buffer.len(); - self.secondary_cursors.retain(|p| *p <= new_len); - self.cursor.set_position(self.cursor.position().min(new_len), &self.buffer); - self.adjust_bookmarks_after_edit(line_before, lines_before); - self.modified = true; - } - /// Undo the last edit. Returns true if anything was undone. pub fn undo(&mut self) -> bool { let r = self.buffer.undo(); @@ -1277,338 +1069,6 @@ impl Editor { r } - /// Dispatch a menu-bar [`crate::editor::menubar::EditorCmd`] to - /// the corresponding editor method. Returns the resulting - /// [`EditorResult`]. Used by F9 menubar dispatch. - pub(crate) fn dispatch_editor_cmd( - &mut self, - cmd: crate::editor::menubar::EditorCmd, - ) -> EditorResult { - use crate::editor::menubar::EditorCmd; - match cmd { - EditorCmd::Undo => { - self.undo(); - } - EditorCmd::Redo => { - self.redo(); - } - EditorCmd::WordComplete => { - self.word_complete(); - } - EditorCmd::SaveSettings => { - self.message = Some("Save via F9→Options→Save setup".to_string()); - } - EditorCmd::Refresh => { - // TLC redraws every frame; CK_Refresh is a no-op. - } - EditorCmd::SpellCheck => { - let misspelled = self.find_next_misspelled( - self.cursor.position(), - ); - if let Some((start, end)) = misspelled { - self.cursor.set_position(end, &self.buffer); - self.cursor.start_selection(); - self.cursor.set_position(start, &self.buffer); - self.message = Some( - "Misspelled word — press Alt-Tab for suggestions" - .to_string(), - ); - } else { - self.message = - Some("No more misspelled words".to_string()); - } - } - EditorCmd::Cut => { - if let Some(text) = self.cursor.selected_text(&self.buffer) { - self.clipboard = Some(text.clone()); - let _ = crate::editor::clipboard_osc52::osc52_copy(&text); - self.cursor.delete_selection(&mut self.buffer); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = self.buffer.is_modified(); - self.message = Some("Block cut".to_string()); - } - } - EditorCmd::Copy => { - if let Some(text) = self.cursor.selected_text(&self.buffer) { - self.clipboard = Some(text.clone()); - let _ = crate::editor::clipboard_osc52::osc52_copy(&text); - self.message = Some("Block copied".to_string()); - } - } - EditorCmd::ToggleColumnMode => { - if self.cursor.selection_mode() == crate::editor::cursor::SelectionMode::Column { - self.cursor.clear_selection(); - self.message = Some("Stream selection".to_string()); - } else { - self.cursor.start_column_selection(); - self.message = Some("Column selection".to_string()); - } - } - EditorCmd::Delete => { - if self.cursor.has_selection() { - self.cursor.delete_selection(&mut self.buffer); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = self.buffer.is_modified(); - self.message = Some("Deleted".to_string()); - } else { - // Delete forward one character. - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - let _ = self.cursor.select_right(&mut self.buffer); - if self.cursor.has_selection() { - self.cursor.delete_selection(&mut self.buffer); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.modified = self.buffer.is_modified(); - } - } - } - EditorCmd::Paste => { - if let Some(text) = self.clipboard.clone() { - if self.cursor.has_selection() { - self.cursor.delete_selection(&mut self.buffer); - } - self.insert_str(&text); - self.message = Some("Pasted".to_string()); - } - } - EditorCmd::GotoTop => { - self.cursor.set_position(0, &self.buffer); - } - EditorCmd::GotoBottom => { - self.cursor.set_position(self.buffer.len(), &self.buffer); - } - EditorCmd::BookmarkToggle => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::BookmarkSet); - } - EditorCmd::BookmarkClearAll => { - let names = self.bookmarks.names(); - let count = names.len(); - for n in names { - self.bookmarks.clear(n); - } - self.message = Some(format!("{count} bookmarks cleared")); - } - EditorCmd::GotoLine => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::GotoLine); - } - EditorCmd::ToggleAutoIndent => { - let on = self.toggle_auto_indent(); - self.message = Some(if on { - "Auto-indent: ON".to_string() - } else { - "Auto-indent: OFF".to_string() - }); - } - EditorCmd::FormatParagraph => { - self.format_paragraph(); - } - EditorCmd::ToggleShowWhitespace => { - let on = self.toggle_show_whitespace(); - self.message = Some(if on { - "Show whitespace: ON".to_string() - } else { - "Show whitespace: OFF".to_string() - }); - } - EditorCmd::ToggleWordWrap => { - let on = self.toggle_word_wrap(); - self.message = Some(if on { - "Word wrap: ON".to_string() - } else { - "Word wrap: OFF".to_string() - }); - } - EditorCmd::ToggleSyntax => { - self.syntax_enabled = !self.syntax_enabled; - self.message = Some(if self.syntax_enabled { - "Syntax: ON".to_string() - } else { - "Syntax: OFF".to_string() - }); - } - EditorCmd::Settings => { - let ai = if self.auto_indent { "ON" } else { "OFF" }; - let ww = if self.word_wrap { "ON" } else { "OFF" }; - let ws = if self.show_whitespace { "ON" } else { "OFF" }; - let sy = if self.syntax_enabled { "ON" } else { "OFF" }; - self.message = Some(format!( - "Settings: auto-indent={ai} wrap={ww} whitespace={ws} syntax={sy} (Alt-A/Alt-W/Alt-E/Ctrl-S toggle)" - )); - } - EditorCmd::New => { - let mut new_editor = Self::new_empty(); - std::mem::swap(&mut self.buffer, &mut new_editor.buffer); - std::mem::swap(&mut self.cursor, &mut new_editor.cursor); - self.path = None; - self.modified = false; - self.title = "*new*".to_string(); - self.message = Some("New buffer".to_string()); - } - EditorCmd::Open => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::InsertFile); - } - EditorCmd::Save => { - match self.save() { - Ok(()) => self.message = Some("Saved".to_string()), - Err(e) => self.message = Some(format!("Save error: {e}")), - } - } - EditorCmd::SaveAs => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::SaveAs); - } - EditorCmd::Quit => { - if self.modified { - self.open_save_before_close_prompt(); - } else { - return EditorResult::Close; - } - } - EditorCmd::Find => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::Find); - } - EditorCmd::FindNext => { - let from = self.cursor.position(); - if let Some(m) = self.search.find_next(&self.buffer, from) { - self.buffer.set_cursor(m.range.start); - self.cursor.set_position(m.range.start, &self.buffer); - self.message = Some( - if self.search.last_wrapped() { - "Search wrapped".to_string() - } else { - "Match found".to_string() - }, - ); - } else { - self.message = Some("No more matches".to_string()); - } - } - EditorCmd::FindPrev => { - let from = self.cursor.position(); - if let Some(m) = self.search.find_prev(&self.buffer, from) { - self.buffer.set_cursor(m.range.start); - self.cursor.set_position(m.range.start, &self.buffer); - self.message = Some( - if self.search.last_wrapped() { - "Search wrapped".to_string() - } else { - "Match found".to_string() - }, - ); - } else { - self.message = Some("No more matches".to_string()); - } - } - EditorCmd::Replace => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::Replace); - } - EditorCmd::BookmarkNext => { - let cur_line = self.buffer.line_of_cursor() as u32; - let all: Vec<(char, crate::editor::bookmark::Mark)> = self - .bookmarks - .names() - .into_iter() - .filter_map(|n| self.bookmarks.get(n).map(|m| (n, m))) - .collect(); - let target = all - .iter() - .find(|(_, m)| m.line > cur_line) - .or_else(|| all.first()); - if let Some((name, mark)) = target { - if let Ok(off) = crate::editor::goto::col_to_offset( - &self.buffer, - mark.line + 1, - mark.col + 1, - ) { - self.buffer.set_cursor(off); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.message = Some(format!("Bookmark '{}'", name)); - } - } else { - self.message = Some("No bookmarks set".to_string()); - } - } - EditorCmd::BookmarkPrev => { - let cur_line = self.buffer.line_of_cursor() as u32; - let mut all: Vec<(char, crate::editor::bookmark::Mark)> = self - .bookmarks - .names() - .into_iter() - .filter_map(|n| self.bookmarks.get(n).map(|m| (n, m))) - .collect(); - all.sort_by_key(|(_, m)| m.line); - let target = all - .iter() - .rev() - .find(|(_, m)| m.line < cur_line) - .or_else(|| all.last()); - if let Some((name, mark)) = target { - if let Ok(off) = crate::editor::goto::col_to_offset( - &self.buffer, - mark.line + 1, - mark.col + 1, - ) { - self.buffer.set_cursor(off); - self.cursor.set_position(self.buffer.cursor(), &self.buffer); - self.message = Some(format!("Bookmark '{}'", name)); - } - } else { - self.message = Some("No bookmarks set".to_string()); - } - } - EditorCmd::EditUserMenu => { - let menu = crate::filemanager::usermenu::UserMenu::new(); - let path = menu.storage_path.clone(); - let new_editor = Self::open(&path); - *self = new_editor; - self.message = Some(format!("Editing user menu: {}", path.display())); - } - EditorCmd::MarkAll => { - let len = self.buffer.len(); - self.cursor.set_position(0, &self.buffer); - self.cursor.start_selection(); - self.cursor.set_position(len, &self.buffer); - } - EditorCmd::InsertFile => { - self.prompt_input.clear(); - self.mode = Mode::Prompt(PromptKind::InsertFile); - } - EditorCmd::BlockSave => { - if let Some(text) = self.cursor.selected_text(&self.buffer) { - self.prompt_input.clear(); - self.prompt_input.text = text; - self.prompt_input.cursor = self.prompt_input.text.chars().count(); - self.mode = Mode::Prompt(PromptKind::SaveAs); - } else { - self.message = Some("No selection".to_string()); - } - } - EditorCmd::ToggleOverwrite => { - self.overwrite = !self.overwrite; - self.message = Some(if self.overwrite { - "[OVR]".to_string() - } else { - "[INS]".to_string() - }); - } - EditorCmd::Close => { - if self.modified { - self.mode = Mode::Prompt(PromptKind::SaveBeforeClose); - } else { - *self = Self::new_empty(); - } - } - _ => { - self.message = Some(format!("F9: {:?} (not yet wired)", cmd)); - } - } - EditorResult::Running - } - /// Alt-P — reformat the current paragraph. /// /// Walks the contiguous block of non-blank lines that contains