Refactor: split editor/mod.rs monolith (4011→3471 lines)

Extracted two impl Editor blocks from the monolithic editor/mod.rs:

- editor/dispatch.rs (331 lines): dispatch_editor_cmd() — all F9 menubar
  EditorCmd variants routed to editor methods. The largest single method
  in the file, now self-contained with its own module docs.

- editor/edit.rs (214 lines): Text editing primitives — insert_char,
  insert_str, delete_back, delete_forward, insert_tab_with_indent,
  multi-cursor operations (insert/delete at all positions). Plus
  private helpers adjust_bookmarks_after_edit and is_in_indent_zone.

mod.rs now holds: struct definition, constructors, getters/setters,
undo/redo, save, format, search, bookmark, spell, bracket, goto,
smooth scroll, user menu, sort_block, and the test module.

1455 tests pass, zero warnings. Zero behavioral changes.
This commit is contained in:
2026-07-08 21:54:30 +03:00
parent 91ad084cb8
commit b9fecc665b
3 changed files with 548 additions and 543 deletions
@@ -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
}
}
@@ -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<usize> {
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;
}
}
+3 -543
View File
@@ -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<usize> {
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