editor_core/model.rs
1//! Command data model types.
2
3use crate::intervals::StyleId;
4use crate::layout::{WrapIndent, WrapMode};
5use crate::search::SearchOptions;
6use crate::snapshot::HeadlessGrid;
7use editor_core_lang::{CommentConfig, IndentationConfig};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10use std::cmp::Ordering;
11
12const COMMAND_HISTORY_TEXT_PREVIEW_BYTES: usize = 256;
13
14/// Position coordinates (line and column numbers)
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct Position {
18 /// Zero-based logical line index.
19 pub line: usize,
20 /// Zero-based column in characters within the logical line.
21 pub column: usize,
22}
23
24impl Position {
25 /// Create a new logical position.
26 pub fn new(line: usize, column: usize) -> Self {
27 Self { line, column }
28 }
29}
30
31impl Ord for Position {
32 fn cmp(&self, other: &Self) -> Ordering {
33 self.line
34 .cmp(&other.line)
35 .then_with(|| self.column.cmp(&other.column))
36 }
37}
38
39impl PartialOrd for Position {
40 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
41 Some(self.cmp(other))
42 }
43}
44
45/// Selection range
46#[derive(Debug, Clone, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub struct Selection {
49 /// Selection start position
50 pub start: Position,
51 /// Selection end position
52 pub end: Position,
53 /// Selection direction
54 pub direction: SelectionDirection,
55}
56
57/// Selection direction
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60pub enum SelectionDirection {
61 /// Forward selection (from start to end)
62 Forward,
63 /// Backward selection (from end to start)
64 Backward,
65}
66
67/// Selection expansion unit for [`CursorCommand::ExpandSelectionBy`].
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum ExpandSelectionUnit {
70 /// Expand by Unicode scalar values (Rust `char` indices).
71 Character,
72 /// Expand by "word" units (configured word boundary rules).
73 Word,
74 /// Expand by logical lines.
75 Line,
76}
77
78/// Selection expansion direction for [`CursorCommand::ExpandSelectionBy`].
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ExpandSelectionDirection {
81 /// Expand towards the beginning of the document.
82 Backward,
83 /// Expand towards the end of the document.
84 Forward,
85}
86
87/// Controls how a Tab key press is handled by the editor when using [`EditCommand::InsertTab`].
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TabKeyBehavior {
90 /// Insert a literal tab character (`'\t'`).
91 Tab,
92 /// Insert spaces up to the next tab stop (based on the current `tab_width` setting).
93 Spaces,
94}
95
96/// A single auto-pair entry (opening + closing delimiter).
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
99pub struct AutoPair {
100 /// Opening delimiter.
101 pub open: char,
102 /// Closing delimiter.
103 pub close: char,
104}
105
106impl AutoPair {
107 /// Create a new auto-pair entry.
108 pub const fn new(open: char, close: char) -> Self {
109 Self { open, close }
110 }
111}
112
113/// Auto-pairs configuration used by [`EditCommand::TypeChar`], and optionally by delete-like
114/// commands (pair deletion).
115#[derive(Debug, Clone, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub struct AutoPairsConfig {
118 /// Master enable switch for auto-pairs behaviors.
119 pub enabled: bool,
120 /// Configured delimiter pairs (order matters when overlapping; first match wins).
121 pub pairs: Vec<AutoPair>,
122 /// When typing an opening delimiter over a non-empty selection, wrap the selection.
123 pub wrap_selection: bool,
124 /// When typing a closing delimiter and the next character matches, skip over it instead of inserting.
125 pub skip_over_closing: bool,
126 /// When backspacing/deleting adjacent matching delimiters, delete both.
127 pub delete_pair: bool,
128}
129
130impl Default for AutoPairsConfig {
131 fn default() -> Self {
132 Self {
133 enabled: false,
134 pairs: vec![
135 AutoPair::new('(', ')'),
136 AutoPair::new('[', ']'),
137 AutoPair::new('{', '}'),
138 AutoPair::new('"', '"'),
139 AutoPair::new('\'', '\''),
140 AutoPair::new('`', '`'),
141 ],
142 wrap_selection: true,
143 skip_over_closing: true,
144 delete_pair: true,
145 }
146 }
147}
148
149impl AutoPairsConfig {
150 pub(super) fn close_for_open(&self, open: char) -> Option<char> {
151 self.pairs.iter().find(|p| p.open == open).map(|p| p.close)
152 }
153
154 pub(super) fn open_for_close(&self, close: char) -> Option<char> {
155 self.pairs.iter().find(|p| p.close == close).map(|p| p.open)
156 }
157
158 pub(super) fn is_matching_pair(&self, open: char, close: char) -> bool {
159 self.pairs
160 .iter()
161 .any(|p| p.open == open && p.close == close)
162 }
163}
164
165/// A simple document text edit (character offsets, half-open).
166///
167/// This is commonly used for applying a batch of "simultaneous" edits (e.g. rename, refactor, or
168/// workspace-wide search/replace), where the edit list is expressed in **pre-edit** coordinates.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct TextEditSpec {
171 /// Inclusive start character offset.
172 pub start: usize,
173 /// Exclusive end character offset.
174 pub end: usize,
175 /// Replacement text.
176 pub text: String,
177}
178
179impl TextEditSpec {
180 fn history_summary(&self) -> Self {
181 Self {
182 start: self.start,
183 end: self.end,
184 text: summarize_history_text(&self.text),
185 }
186 }
187}
188
189fn summarize_history_text(text: &str) -> String {
190 if text.len() <= COMMAND_HISTORY_TEXT_PREVIEW_BYTES {
191 return text.to_string();
192 }
193
194 let mut preview_end = 0;
195 for (byte_idx, ch) in text.char_indices() {
196 let next = byte_idx + ch.len_utf8();
197 if next > COMMAND_HISTORY_TEXT_PREVIEW_BYTES {
198 break;
199 }
200 preview_end = next;
201 }
202
203 format!(
204 "{}...[history truncated: {} bytes]",
205 &text[..preview_end],
206 text.len()
207 )
208}
209
210/// Text editing commands
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum EditCommand {
213 /// Insert text at the specified position
214 Insert {
215 /// Character offset to insert at.
216 offset: usize,
217 /// Text to insert.
218 text: String,
219 },
220 /// Delete text in specified range
221 Delete {
222 /// Character offset of the deletion start.
223 start: usize,
224 /// Length of the deletion in characters.
225 length: usize,
226 },
227 /// Replace text in specified range
228 Replace {
229 /// Character offset of the replacement start.
230 start: usize,
231 /// Length of the replaced range in characters.
232 length: usize,
233 /// Replacement text.
234 text: String,
235 },
236 /// Replace text in specified range, requesting undo coalescing when the edit is eligible.
237 ///
238 /// This is primarily useful for UI layers that need to keep IME composition updates and the
239 /// final commit in one explicitly delimited undo group.
240 ///
241 /// Notes:
242 /// - Normal typing coalescing is intentionally limited to pure adjacent insertions without
243 /// newlines.
244 /// - This explicit composition path may coalesce replacements only when each update replaces
245 /// the exact range inserted by the previous update and the selection state is continuous.
246 /// - The caller is expected to explicitly delimit boundaries via [`EditCommand::EndUndoGroup`]
247 /// so eligible IME insertions do not merge with normal typing groups.
248 ReplaceCoalescingUndo {
249 /// Character offset of the replacement start.
250 start: usize,
251 /// Length of the replaced range in characters.
252 length: usize,
253 /// Replacement text.
254 text: String,
255 },
256 /// Like [`EditCommand::ReplaceCoalescingUndo`], but also sets the primary selection/caret.
257 ///
258 /// This is primarily useful for IME composition updates where the host provides a
259 /// selection range inside the marked (preedit) string while keeping composition updates in
260 /// one explicitly delimited undo group.
261 ///
262 /// Notes:
263 /// - `selection_start/selection_end` are **post-edit** character offsets (Unicode scalar indices)
264 /// in the resulting document.
265 /// - If `selection_start == selection_end`, the selection is cleared and the caret is moved
266 /// to `selection_end`.
267 ReplaceCoalescingUndoWithSelection {
268 /// Character offset of the replacement start.
269 start: usize,
270 /// Length of the replaced range in characters.
271 length: usize,
272 /// Replacement text.
273 text: String,
274 /// Selection start (post-edit) in character offsets.
275 selection_start: usize,
276 /// Selection end (post-edit) in character offsets.
277 selection_end: usize,
278 },
279 /// VSCode-like typing/paste: apply to all carets/selections (primary + secondary)
280 InsertText {
281 /// Text to insert/replace at each selection/caret.
282 text: String,
283 },
284 /// Type a single character using auto-pairs rules (if enabled).
285 ///
286 /// This is intended for UI "typing" paths (not paste). It supports:
287 /// - auto-close pairs (`()`, `{}`, `[]`, quotes)
288 /// - skip over existing closing delimiters
289 /// - wrap selection with pairs (optional)
290 TypeChar {
291 /// The typed character.
292 ch: char,
293 },
294 /// Insert a tab at each caret (or replace each selection), using the current tab settings.
295 ///
296 /// - If `TabKeyBehavior::Tab`, inserts `'\t'`.
297 /// - If `TabKeyBehavior::Spaces`, inserts spaces up to the next tab stop.
298 InsertTab,
299 /// Insert a newline at each caret (or replace each selection).
300 ///
301 /// If `auto_indent` is true, the inserted newline is followed by the leading whitespace
302 /// prefix of the current logical line.
303 InsertNewline {
304 /// Whether to auto-indent the new line.
305 auto_indent: bool,
306 },
307 /// Indent the selected lines (or the current line for an empty selection).
308 Indent,
309 /// Outdent the selected lines (or the current line for an empty selection).
310 Outdent,
311 /// Duplicate the selected line(s) (or the current line for an empty selection).
312 ///
313 /// This is a line-based operation and will act on all carets/selections (primary + secondary),
314 /// including rectangular selections.
315 DuplicateLines,
316 /// Delete the selected line(s) (or the current line for an empty selection).
317 ///
318 /// This is a line-based operation and will act on all carets/selections (primary + secondary),
319 /// including rectangular selections.
320 DeleteLines,
321 /// Move the selected line(s) up by one line.
322 ///
323 /// This is a line-based operation and will act on all carets/selections (primary + secondary),
324 /// including rectangular selections.
325 MoveLinesUp,
326 /// Move the selected line(s) down by one line.
327 ///
328 /// This is a line-based operation and will act on all carets/selections (primary + secondary),
329 /// including rectangular selections.
330 MoveLinesDown,
331 /// Join the current line with the next line (for each caret/selection).
332 ///
333 /// If multiple carets/selections exist, joins are applied from bottom to top to keep offsets stable.
334 JoinLines,
335 /// Split the current line at each caret (or replace each selection) by inserting a newline.
336 ///
337 /// This is a convenience alias for [`EditCommand::InsertNewline`] with `auto_indent: false`.
338 SplitLine,
339 /// Toggle comments for the selected line(s) or selection ranges, using a language-provided
340 /// comment configuration.
341 ToggleComment {
342 /// Comment tokens/config for the current language (data-driven).
343 config: CommentConfig,
344 },
345 /// Apply a batch of text edits as a single undoable step.
346 ///
347 /// - Edits are interpreted in **pre-edit** character offsets.
348 /// - Edits must be non-overlapping; they are applied in descending offset order internally.
349 ApplyTextEdits {
350 /// The edit list (character offsets, half-open).
351 edits: Vec<TextEditSpec>,
352 },
353 /// Apply a snippet-shaped insert as a single undoable step.
354 ///
355 /// This is primarily intended for LSP completion items with `insertTextFormat == 2`.
356 ///
357 /// - `start`/`end` are interpreted in **pre-edit** character offsets (half-open).
358 /// - `additional_edits` are applied in the same undo step (also in pre-edit coordinates).
359 /// - The snippet is expanded (placeholders removed / defaults inserted), and the first
360 /// placeholder (lowest index) is selected for navigation via
361 /// [`CursorCommand::SnippetNextPlaceholder`] / [`CursorCommand::SnippetPrevPlaceholder`].
362 ApplySnippet {
363 /// Inclusive start character offset.
364 start: usize,
365 /// Exclusive end character offset.
366 end: usize,
367 /// Snippet text in TextMate / VS Code snippet syntax.
368 snippet: String,
369 /// Additional text edits (LSP `additionalTextEdits`), in pre-edit coordinates.
370 additional_edits: Vec<TextEditSpec>,
371 },
372 /// Smart backspace: if the caret is in leading whitespace, delete back to the previous tab stop.
373 ///
374 /// Otherwise, behaves like [`EditCommand::Backspace`].
375 DeleteToPrevTabStop,
376 /// Delete the previous Unicode grapheme cluster (UAX #29) for each caret/selection.
377 DeleteGraphemeBack,
378 /// Delete the next Unicode grapheme cluster (UAX #29) for each caret/selection.
379 DeleteGraphemeForward,
380 /// Delete back to the previous Unicode word boundary (UAX #29) for each caret/selection.
381 DeleteWordBack,
382 /// Delete forward to the next Unicode word boundary (UAX #29) for each caret/selection.
383 DeleteWordForward,
384 /// Backspace-like deletion: delete selection(s) if any, otherwise delete 1 char before each caret.
385 Backspace,
386 /// Delete key-like deletion: delete selection(s) if any, otherwise delete 1 char after each caret.
387 DeleteForward,
388 /// Undo last edit operation (supports grouping)
389 Undo,
390 /// Redo last undone operation (supports grouping)
391 Redo,
392 /// Explicitly end the current undo group (for idle or external boundaries)
393 EndUndoGroup,
394 /// Replace the current occurrence of `query` (based on selection/caret) with `replacement`.
395 ///
396 /// - Honors `options` (case sensitivity / whole-word / regex).
397 /// - Treated as a single undoable edit.
398 ReplaceCurrent {
399 /// Search query.
400 query: String,
401 /// Replacement text.
402 replacement: String,
403 /// Search options (case sensitivity, whole-word, regex).
404 options: SearchOptions,
405 },
406 /// Replace all occurrences of `query` with `replacement`.
407 ///
408 /// - Honors `options` (case sensitivity / whole-word / regex).
409 /// - Treated as a single undoable edit.
410 ReplaceAll {
411 /// Search query.
412 query: String,
413 /// Replacement text.
414 replacement: String,
415 /// Search options (case sensitivity, whole-word, regex).
416 options: SearchOptions,
417 },
418}
419
420/// Cursor & selection commands
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub enum CursorCommand {
423 /// Move cursor to the specified position
424 MoveTo {
425 /// Target logical line index.
426 line: usize,
427 /// Target column in characters (will be clamped to line length).
428 column: usize,
429 },
430 /// Move cursor relatively
431 MoveBy {
432 /// Delta in logical lines.
433 delta_line: isize,
434 /// Delta in columns (characters).
435 delta_column: isize,
436 },
437 /// Move cursor by visual rows (soft wrap + folding aware).
438 ///
439 /// This uses a "preferred x" in **cells** (sticky column) similar to many editors:
440 /// horizontal moves update preferred x, while vertical visual moves try to preserve it.
441 MoveVisualBy {
442 /// Delta in global visual rows (after wrapping/folding).
443 delta_rows: isize,
444 },
445 /// Move cursor to a visual position (global visual row + x in cells).
446 MoveToVisual {
447 /// Target global visual row (after wrapping/folding).
448 row: usize,
449 /// Target x offset in cells within that visual row.
450 x_cells: usize,
451 },
452 /// Move cursor to the start of the current logical line.
453 MoveToLineStart,
454 /// Move cursor to the end of the current logical line.
455 MoveToLineEnd,
456 /// Move cursor to the start of the current visual line segment (wrap-aware).
457 MoveToVisualLineStart,
458 /// Move cursor to the end of the current visual line segment (wrap-aware).
459 MoveToVisualLineEnd,
460 /// Move cursor left by one Unicode grapheme cluster (UAX #29).
461 MoveGraphemeLeft,
462 /// Move cursor right by one Unicode grapheme cluster (UAX #29).
463 MoveGraphemeRight,
464 /// Move cursor left to the previous Unicode word boundary (UAX #29).
465 MoveWordLeft,
466 /// Move cursor right to the next Unicode word boundary (UAX #29).
467 MoveWordRight,
468 /// Move each caret to its matching bracket (if the caret is on or adjacent to a bracket).
469 ///
470 /// Matching is performed for the configured bracket pairs (typically `()`, `[]`, `{}`).
471 MoveToMatchingBracket,
472 /// If a snippet session is active, jump to the **next** snippet tabstop (placeholder).
473 ///
474 /// This is typically bound to the Tab key while a completion snippet is active.
475 SnippetNextPlaceholder,
476 /// If a snippet session is active, jump to the **previous** snippet tabstop (placeholder).
477 ///
478 /// This is typically bound to Shift-Tab while a completion snippet is active.
479 SnippetPrevPlaceholder,
480 /// Set selection range
481 SetSelection {
482 /// Selection start position.
483 start: Position,
484 /// Selection end position.
485 end: Position,
486 },
487 /// Extend selection range
488 ExtendSelection {
489 /// New active end position.
490 to: Position,
491 },
492 /// Clear selection
493 ClearSelection,
494 /// Set multiple selections/multi-cursor (including primary)
495 SetSelections {
496 /// All selections (including primary).
497 selections: Vec<Selection>,
498 /// Index of the primary selection in `selections`.
499 primary_index: usize,
500 },
501 /// Clear secondary selections/cursors, keeping only primary
502 ClearSecondarySelections,
503 /// Set rectangular selection (box/column selection), which expands into one Selection per line
504 SetRectSelection {
505 /// Anchor position (fixed corner).
506 anchor: Position,
507 /// Active position (moving corner).
508 active: Position,
509 },
510 /// Select the entire current line (or the set of lines covered by the selection), for all carets.
511 SelectLine,
512 /// Select the word under each caret (or keep existing selections if already non-empty).
513 SelectWord,
514 /// Expand selection in a basic, editor-friendly way.
515 ///
516 /// - If the selection is empty, expands to the word under the caret.
517 /// - If the selection is non-empty, expands to full line(s).
518 ExpandSelection,
519 /// Expand selection by a configurable unit and direction.
520 ///
521 /// Notes:
522 /// - This is an **expand-only** operation: it never shrinks the current selection.
523 /// - The expansion direction is absolute (backward/forward in document order). If you call
524 /// it with different directions over time, the selection can expand on both ends.
525 ExpandSelectionBy {
526 /// Expansion unit.
527 unit: ExpandSelectionUnit,
528 /// Number of units to expand by. `0` is a no-op.
529 count: usize,
530 /// Expansion direction in document order.
531 direction: ExpandSelectionDirection,
532 },
533 /// Add a new caret above each existing caret/selection (at the same column, clamped to line length).
534 AddCursorAbove,
535 /// Add a new caret below each existing caret/selection (at the same column, clamped to line length).
536 AddCursorBelow,
537 /// Multi-cursor match op: add the next occurrence of the current selection/word as a new selection.
538 AddNextOccurrence {
539 /// Search options (case sensitivity, whole-word, regex).
540 options: SearchOptions,
541 },
542 /// Multi-cursor match op: select all occurrences of the current selection/word.
543 AddAllOccurrences {
544 /// Search options (case sensitivity, whole-word, regex).
545 options: SearchOptions,
546 },
547 /// Find the next occurrence of `query` and select it (primary selection only).
548 FindNext {
549 /// Search query.
550 query: String,
551 /// Search options (case sensitivity, whole-word, regex).
552 options: SearchOptions,
553 },
554 /// Find the previous occurrence of `query` and select it (primary selection only).
555 FindPrev {
556 /// Search query.
557 query: String,
558 /// Search options (case sensitivity, whole-word, regex).
559 options: SearchOptions,
560 },
561}
562
563/// View commands
564#[derive(Debug, Clone, PartialEq, Eq)]
565pub enum ViewCommand {
566 /// Set viewport width
567 SetViewportWidth {
568 /// Width in character cells.
569 width: usize,
570 },
571 /// Set soft wrap mode.
572 SetWrapMode {
573 /// Wrap mode.
574 mode: WrapMode,
575 },
576 /// Set wrapped-line indentation policy.
577 SetWrapIndent {
578 /// Wrap indent policy.
579 indent: WrapIndent,
580 },
581 /// Set tab width (in character cells) used for measuring `'\t'` and tab stops.
582 SetTabWidth {
583 /// Tab width in character cells (must be greater than 0).
584 width: usize,
585 },
586 /// Configure how [`EditCommand::InsertTab`] inserts text.
587 SetTabKeyBehavior {
588 /// Tab key behavior.
589 behavior: TabKeyBehavior,
590 },
591 /// Configure language-aware auto-indentation behavior used by [`EditCommand::InsertNewline`]
592 /// when `auto_indent=true`.
593 ///
594 /// Notes:
595 /// - This is view-local (each view can have different indentation rules).
596 SetIndentationConfig {
597 /// Indentation configuration.
598 config: IndentationConfig,
599 },
600 /// Configure auto-pairs behavior used by [`EditCommand::TypeChar`].
601 SetAutoPairsConfig {
602 /// Auto-pairs config.
603 config: AutoPairsConfig,
604 },
605 /// Enable/disable auto-pairs behavior (convenience wrapper over [`ViewCommand::SetAutoPairsConfig`]).
606 SetAutoPairsEnabled {
607 /// Whether auto-pairs are enabled.
608 enabled: bool,
609 },
610 /// Override the ASCII word-boundary character set used by editor-friendly "word" operations.
611 ///
612 /// This is similar in spirit to VSCode's `wordSeparators`.
613 ///
614 /// Notes:
615 /// - Only ASCII characters are configurable here; non-ASCII characters are always treated as boundaries.
616 /// - ASCII whitespace is always treated as a boundary.
617 SetWordBoundaryAsciiBoundaryChars {
618 /// ASCII word-boundary characters (as a string of separators).
619 boundary_chars: String,
620 },
621 /// Reset word-boundary configuration to the default (ASCII identifier-like words).
622 ResetWordBoundaryDefaults,
623 /// Scroll to specified line
624 ScrollTo {
625 /// Logical line index to scroll to.
626 line: usize,
627 },
628 /// Get current viewport content
629 GetViewport {
630 /// Starting visual row.
631 start_row: usize,
632 /// Number of visual rows requested.
633 count: usize,
634 },
635}
636
637/// Style and folding commands
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub enum StyleCommand {
640 /// Add style interval
641 AddStyle {
642 /// Interval start offset in characters.
643 start: usize,
644 /// Interval end offset in characters (exclusive).
645 end: usize,
646 /// Style identifier.
647 style_id: StyleId,
648 },
649 /// Remove style interval
650 RemoveStyle {
651 /// Interval start offset in characters.
652 start: usize,
653 /// Interval end offset in characters (exclusive).
654 end: usize,
655 /// Style identifier.
656 style_id: StyleId,
657 },
658 /// Fold code block
659 Fold {
660 /// Start logical line (inclusive).
661 start_line: usize,
662 /// End logical line (inclusive).
663 end_line: usize,
664 },
665 /// Unfold code block
666 Unfold {
667 /// Start logical line (inclusive) of the fold region to unfold.
668 start_line: usize,
669 },
670 /// Unfold all folds
671 UnfoldAll,
672 /// Recompute bracket-match highlights for the current cursor/selections.
673 ///
674 /// This updates the derived style layer [`StyleLayerId::BRACKET_MATCHES`].
675 UpdateBracketMatchHighlights,
676 /// Clear bracket-match highlights (removes [`StyleLayerId::BRACKET_MATCHES`]).
677 ClearBracketMatchHighlights,
678}
679
680/// Unified command enum
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub enum Command {
683 /// Text editing commands
684 Edit(EditCommand),
685 /// Cursor command
686 Cursor(CursorCommand),
687 /// View commands
688 View(ViewCommand),
689 /// Style command
690 Style(StyleCommand),
691}
692
693impl EditCommand {
694 fn is_mutating(&self) -> bool {
695 match self {
696 EditCommand::Insert { .. }
697 | EditCommand::Delete { .. }
698 | EditCommand::Replace { .. }
699 | EditCommand::ReplaceCoalescingUndo { .. }
700 | EditCommand::ReplaceCoalescingUndoWithSelection { .. }
701 | EditCommand::InsertText { .. }
702 | EditCommand::TypeChar { .. }
703 | EditCommand::InsertTab
704 | EditCommand::InsertNewline { .. }
705 | EditCommand::Indent
706 | EditCommand::Outdent
707 | EditCommand::DuplicateLines
708 | EditCommand::DeleteLines
709 | EditCommand::MoveLinesUp
710 | EditCommand::MoveLinesDown
711 | EditCommand::JoinLines
712 | EditCommand::SplitLine
713 | EditCommand::ToggleComment { .. }
714 | EditCommand::ApplyTextEdits { .. }
715 | EditCommand::ApplySnippet { .. }
716 | EditCommand::DeleteToPrevTabStop
717 | EditCommand::DeleteGraphemeBack
718 | EditCommand::DeleteGraphemeForward
719 | EditCommand::DeleteWordBack
720 | EditCommand::DeleteWordForward
721 | EditCommand::Backspace
722 | EditCommand::DeleteForward
723 | EditCommand::Undo
724 | EditCommand::Redo
725 | EditCommand::EndUndoGroup
726 | EditCommand::ReplaceCurrent { .. }
727 | EditCommand::ReplaceAll { .. } => true,
728 }
729 }
730
731 fn history_summary(&self) -> Self {
732 match self {
733 EditCommand::Insert { offset, text } => EditCommand::Insert {
734 offset: *offset,
735 text: summarize_history_text(text),
736 },
737 EditCommand::Delete { start, length } => EditCommand::Delete {
738 start: *start,
739 length: *length,
740 },
741 EditCommand::Replace {
742 start,
743 length,
744 text,
745 } => EditCommand::Replace {
746 start: *start,
747 length: *length,
748 text: summarize_history_text(text),
749 },
750 EditCommand::ReplaceCoalescingUndo {
751 start,
752 length,
753 text,
754 } => EditCommand::ReplaceCoalescingUndo {
755 start: *start,
756 length: *length,
757 text: summarize_history_text(text),
758 },
759 EditCommand::ReplaceCoalescingUndoWithSelection {
760 start,
761 length,
762 text,
763 selection_start,
764 selection_end,
765 } => EditCommand::ReplaceCoalescingUndoWithSelection {
766 start: *start,
767 length: *length,
768 text: summarize_history_text(text),
769 selection_start: *selection_start,
770 selection_end: *selection_end,
771 },
772 EditCommand::InsertText { text } => EditCommand::InsertText {
773 text: summarize_history_text(text),
774 },
775 EditCommand::TypeChar { ch } => EditCommand::TypeChar { ch: *ch },
776 EditCommand::InsertTab => EditCommand::InsertTab,
777 EditCommand::InsertNewline { auto_indent } => EditCommand::InsertNewline {
778 auto_indent: *auto_indent,
779 },
780 EditCommand::Indent => EditCommand::Indent,
781 EditCommand::Outdent => EditCommand::Outdent,
782 EditCommand::DuplicateLines => EditCommand::DuplicateLines,
783 EditCommand::DeleteLines => EditCommand::DeleteLines,
784 EditCommand::MoveLinesUp => EditCommand::MoveLinesUp,
785 EditCommand::MoveLinesDown => EditCommand::MoveLinesDown,
786 EditCommand::JoinLines => EditCommand::JoinLines,
787 EditCommand::SplitLine => EditCommand::SplitLine,
788 EditCommand::ToggleComment { config } => EditCommand::ToggleComment {
789 config: config.clone(),
790 },
791 EditCommand::ApplyTextEdits { edits } => EditCommand::ApplyTextEdits {
792 edits: edits.iter().map(TextEditSpec::history_summary).collect(),
793 },
794 EditCommand::ApplySnippet {
795 start,
796 end,
797 snippet,
798 additional_edits,
799 } => EditCommand::ApplySnippet {
800 start: *start,
801 end: *end,
802 snippet: summarize_history_text(snippet),
803 additional_edits: additional_edits
804 .iter()
805 .map(TextEditSpec::history_summary)
806 .collect(),
807 },
808 EditCommand::DeleteToPrevTabStop => EditCommand::DeleteToPrevTabStop,
809 EditCommand::DeleteGraphemeBack => EditCommand::DeleteGraphemeBack,
810 EditCommand::DeleteGraphemeForward => EditCommand::DeleteGraphemeForward,
811 EditCommand::DeleteWordBack => EditCommand::DeleteWordBack,
812 EditCommand::DeleteWordForward => EditCommand::DeleteWordForward,
813 EditCommand::Backspace => EditCommand::Backspace,
814 EditCommand::DeleteForward => EditCommand::DeleteForward,
815 EditCommand::Undo => EditCommand::Undo,
816 EditCommand::Redo => EditCommand::Redo,
817 EditCommand::EndUndoGroup => EditCommand::EndUndoGroup,
818 EditCommand::ReplaceCurrent {
819 query,
820 replacement,
821 options,
822 } => EditCommand::ReplaceCurrent {
823 query: summarize_history_text(query),
824 replacement: summarize_history_text(replacement),
825 options: *options,
826 },
827 EditCommand::ReplaceAll {
828 query,
829 replacement,
830 options,
831 } => EditCommand::ReplaceAll {
832 query: summarize_history_text(query),
833 replacement: summarize_history_text(replacement),
834 options: *options,
835 },
836 }
837 }
838}
839
840impl CursorCommand {
841 fn is_mutating(&self) -> bool {
842 match self {
843 CursorCommand::MoveTo { .. }
844 | CursorCommand::MoveBy { .. }
845 | CursorCommand::MoveVisualBy { .. }
846 | CursorCommand::MoveToVisual { .. }
847 | CursorCommand::MoveToLineStart
848 | CursorCommand::MoveToLineEnd
849 | CursorCommand::MoveToVisualLineStart
850 | CursorCommand::MoveToVisualLineEnd
851 | CursorCommand::MoveGraphemeLeft
852 | CursorCommand::MoveGraphemeRight
853 | CursorCommand::MoveWordLeft
854 | CursorCommand::MoveWordRight
855 | CursorCommand::MoveToMatchingBracket
856 | CursorCommand::SnippetNextPlaceholder
857 | CursorCommand::SnippetPrevPlaceholder
858 | CursorCommand::SetSelection { .. }
859 | CursorCommand::ExtendSelection { .. }
860 | CursorCommand::ClearSelection
861 | CursorCommand::SetSelections { .. }
862 | CursorCommand::ClearSecondarySelections
863 | CursorCommand::SetRectSelection { .. }
864 | CursorCommand::SelectLine
865 | CursorCommand::SelectWord
866 | CursorCommand::ExpandSelection
867 | CursorCommand::ExpandSelectionBy { .. }
868 | CursorCommand::AddCursorAbove
869 | CursorCommand::AddCursorBelow
870 | CursorCommand::AddNextOccurrence { .. }
871 | CursorCommand::AddAllOccurrences { .. }
872 | CursorCommand::FindNext { .. }
873 | CursorCommand::FindPrev { .. } => false,
874 }
875 }
876
877 fn history_summary(&self) -> Self {
878 match self {
879 CursorCommand::FindNext { query, options } => CursorCommand::FindNext {
880 query: summarize_history_text(query),
881 options: *options,
882 },
883 CursorCommand::FindPrev { query, options } => CursorCommand::FindPrev {
884 query: summarize_history_text(query),
885 options: *options,
886 },
887 _ => self.clone(),
888 }
889 }
890}
891
892impl ViewCommand {
893 fn is_mutating(&self) -> bool {
894 match self {
895 ViewCommand::SetViewportWidth { .. }
896 | ViewCommand::SetWrapMode { .. }
897 | ViewCommand::SetWrapIndent { .. }
898 | ViewCommand::SetTabWidth { .. }
899 | ViewCommand::SetTabKeyBehavior { .. }
900 | ViewCommand::SetIndentationConfig { .. }
901 | ViewCommand::SetAutoPairsConfig { .. }
902 | ViewCommand::SetAutoPairsEnabled { .. }
903 | ViewCommand::SetWordBoundaryAsciiBoundaryChars { .. }
904 | ViewCommand::ResetWordBoundaryDefaults => true,
905 ViewCommand::ScrollTo { .. } | ViewCommand::GetViewport { .. } => false,
906 }
907 }
908
909 fn history_summary(&self) -> Self {
910 match self {
911 ViewCommand::SetWordBoundaryAsciiBoundaryChars { boundary_chars } => {
912 ViewCommand::SetWordBoundaryAsciiBoundaryChars {
913 boundary_chars: summarize_history_text(boundary_chars),
914 }
915 }
916 _ => self.clone(),
917 }
918 }
919}
920
921impl StyleCommand {
922 fn is_mutating(&self) -> bool {
923 match self {
924 StyleCommand::AddStyle { .. }
925 | StyleCommand::RemoveStyle { .. }
926 | StyleCommand::Fold { .. }
927 | StyleCommand::Unfold { .. }
928 | StyleCommand::UnfoldAll
929 | StyleCommand::UpdateBracketMatchHighlights
930 | StyleCommand::ClearBracketMatchHighlights => true,
931 }
932 }
933}
934
935impl Command {
936 /// Returns whether this command mutates document content, history, configuration, or styles.
937 pub fn is_mutating(&self) -> bool {
938 match self {
939 Command::Edit(command) => command.is_mutating(),
940 Command::Cursor(command) => command.is_mutating(),
941 Command::View(command) => command.is_mutating(),
942 Command::Style(command) => command.is_mutating(),
943 }
944 }
945
946 pub(super) fn history_summary(&self) -> Self {
947 match self {
948 Command::Edit(command) => Command::Edit(command.history_summary()),
949 Command::Cursor(command) => Command::Cursor(command.history_summary()),
950 Command::View(command) => Command::View(command.history_summary()),
951 Command::Style(command) => Command::Style(command.clone()),
952 }
953 }
954}
955
956/// Command execution result
957#[derive(Debug, Clone)]
958pub enum CommandResult {
959 /// Success, no return value
960 Success,
961 /// Success, returns text
962 Text(String),
963 /// Success, returns position
964 Position(Position),
965 /// Success, returns offset
966 Offset(usize),
967 /// Success, returns viewport content
968 Viewport(HeadlessGrid),
969 /// Find/search result: a match in char offsets (half-open).
970 SearchMatch {
971 /// Inclusive start character offset.
972 start: usize,
973 /// Exclusive end character offset.
974 end: usize,
975 },
976 /// Find/search result: no match found.
977 SearchNotFound,
978 /// Replace result: how many occurrences were replaced.
979 ReplaceResult {
980 /// Number of occurrences replaced.
981 replaced: usize,
982 },
983}
984
985/// Command error type
986#[derive(Debug, Clone, PartialEq, Eq)]
987pub enum CommandError {
988 /// Invalid offset
989 InvalidOffset(usize),
990 /// Invalid position
991 InvalidPosition {
992 /// Logical line index.
993 line: usize,
994 /// Column in characters.
995 column: usize,
996 },
997 /// Invalid range
998 InvalidRange {
999 /// Inclusive start character offset.
1000 start: usize,
1001 /// Exclusive end character offset.
1002 end: usize,
1003 },
1004 /// Empty text
1005 EmptyText,
1006 /// Other error
1007 Other(String),
1008}
1009
1010impl std::fmt::Display for CommandError {
1011 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1012 match self {
1013 CommandError::InvalidOffset(offset) => {
1014 write!(f, "Invalid offset: {}", offset)
1015 }
1016 CommandError::InvalidPosition { line, column } => {
1017 write!(f, "Invalid position: line {}, column {}", line, column)
1018 }
1019 CommandError::InvalidRange { start, end } => {
1020 write!(f, "Invalid range: {}..{}", start, end)
1021 }
1022 CommandError::EmptyText => {
1023 write!(f, "Text cannot be empty")
1024 }
1025 CommandError::Other(msg) => {
1026 write!(f, "{}", msg)
1027 }
1028 }
1029 }
1030}
1031
1032impl std::error::Error for CommandError {}