use crate::decorations::{Decoration, DecorationLayerId, DecorationPlacement};
use crate::delta::{TextDelta, TextDeltaEdit};
use crate::diagnostics::Diagnostic;
use crate::intervals::{FoldRegion, IntervalTextEdit, StyleId, StyleLayerId};
use crate::layout::{
cell_width_at, char_width, visual_x_for_column, wrap_indent_cells_for_line_text,
};
use crate::line_ending::LineEnding;
use crate::search::{CharIndex, SearchMatch, SearchOptions, find_all, find_next, find_prev};
use crate::snapshot::{
Cell, ComposedCell, ComposedCellSource, ComposedGrid, ComposedLine, ComposedLineKind,
HeadlessGrid, HeadlessLine, MinimapGrid, MinimapLine,
};
use crate::snippets::{SnippetNavigation, SnippetSession, parse_snippet};
#[cfg(debug_assertions)]
use crate::storage::PieceTable;
use crate::visual_rows::VisualRowIndex;
use crate::{FOLD_PLACEHOLDER_STYLE_ID, FoldingManager, IntervalTree, LayoutEngine, LineIndex};
use editor_core_lang::{CommentConfig, IndentStyle, IndentationConfig};
use regex::RegexBuilder;
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::time::Duration;
use unicode_segmentation::UnicodeSegmentation;
const DEFAULT_COMMAND_HISTORY_LIMIT: usize = 1000;
#[path = "model.rs"]
mod model;
pub use self::model::{
AutoPair, AutoPairsConfig, Command, CommandError, CommandResult, CursorCommand, EditCommand,
ExpandSelectionDirection, ExpandSelectionUnit, Position, Selection, SelectionDirection,
StyleCommand, TabKeyBehavior, TextEditSpec, ViewCommand,
};
#[path = "undo.rs"]
mod undo;
use self::undo::{TextEdit, UndoRedoManager, UndoStep};
pub use self::undo::{
UndoHistoryRestoreError, UndoHistorySelectionSet, UndoHistorySnapshot, UndoHistoryStep,
UndoHistoryTextEdit,
};
#[path = "render_grid.rs"]
mod render_grid;
#[path = "cursor_ops.rs"]
mod cursor_ops;
pub use self::cursor_ops::WordBoundaryConfig;
use self::cursor_ops::{
TextBoundary, leading_horizontal_whitespace, next_boundary_column, prev_boundary_column,
};
#[path = "line_ops.rs"]
mod line_ops;
#[path = "edit_ops.rs"]
mod edit_ops;
#[derive(Debug, Clone, PartialEq, Eq)]
struct SelectionSetSnapshot {
selections: Vec<Selection>,
primary_index: usize,
}
pub struct EditorCore {
#[cfg(debug_assertions)]
piece_table_shadow: PieceTable,
line_index: LineIndex,
layout_engine: LayoutEngine,
interval_tree: IntervalTree,
style_layers: BTreeMap<StyleLayerId, IntervalTree>,
diagnostics: Vec<Diagnostic>,
decorations: BTreeMap<DecorationLayerId, Vec<Decoration>>,
document_symbols: crate::DocumentOutline,
folding_manager: FoldingManager,
cursor_position: Position,
selection: Option<Selection>,
secondary_selections: Vec<Selection>,
viewport_width: usize,
word_boundary: WordBoundaryConfig,
visual_row_index_cache: RefCell<Option<VisualRowIndex>>,
}
impl EditorCore {
pub fn new(text: &str, viewport_width: usize) -> Self {
let normalized = crate::text::normalize_crlf_to_lf(text);
let text = normalized.as_ref();
#[cfg(debug_assertions)]
let piece_table_shadow = PieceTable::new(text);
let line_index = LineIndex::from_text(text);
let mut layout_engine = LayoutEngine::new(viewport_width);
let lines = crate::text::split_lines_preserve_trailing(text);
let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
layout_engine.from_lines(&line_refs);
Self {
#[cfg(debug_assertions)]
piece_table_shadow,
line_index,
layout_engine,
interval_tree: IntervalTree::new(),
style_layers: BTreeMap::new(),
diagnostics: Vec::new(),
decorations: BTreeMap::new(),
document_symbols: crate::DocumentOutline::default(),
folding_manager: FoldingManager::new(),
cursor_position: Position::new(0, 0),
selection: None,
secondary_selections: Vec::new(),
viewport_width,
word_boundary: WordBoundaryConfig::default(),
visual_row_index_cache: RefCell::new(None),
}
}
pub fn empty(viewport_width: usize) -> Self {
Self::new("", viewport_width)
}
pub fn get_text(&self) -> String {
self.line_index.text_buffer().get_text()
}
pub fn text_range(&self, start: usize, len: usize) -> String {
self.line_index.text_buffer().get_range(start, len)
}
pub fn line_count(&self) -> usize {
self.line_index.line_count()
}
pub fn char_count(&self) -> usize {
self.line_index.text_buffer().len_chars()
}
pub fn set_word_boundary_ascii_boundary_chars(&mut self, boundary_chars: &str) {
self.word_boundary.set_ascii_boundary_chars(boundary_chars);
}
pub fn reset_word_boundary_defaults(&mut self) {
self.word_boundary = WordBoundaryConfig::default();
}
pub fn cursor_position(&self) -> Position {
self.cursor_position
}
pub fn selection(&self) -> Option<&Selection> {
self.selection.as_ref()
}
pub fn secondary_selections(&self) -> &[Selection] {
&self.secondary_selections
}
pub(crate) fn set_cursor_state(
&mut self,
cursor_position: Position,
selection: Option<Selection>,
secondary_selections: Vec<Selection>,
) {
self.cursor_position = cursor_position;
self.selection = selection;
self.secondary_selections = secondary_selections;
}
pub fn line_index(&self) -> &LineIndex {
&self.line_index
}
pub fn layout_engine(&self) -> &LayoutEngine {
&self.layout_engine
}
pub fn interval_tree(&self) -> &IntervalTree {
&self.interval_tree
}
pub fn style_layers(&self) -> &BTreeMap<StyleLayerId, IntervalTree> {
&self.style_layers
}
pub fn style_layer(&self, layer: StyleLayerId) -> Option<&IntervalTree> {
self.style_layers.get(&layer)
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn decorations(&self) -> &BTreeMap<DecorationLayerId, Vec<Decoration>> {
&self.decorations
}
pub fn decorations_for_layer(&self, layer: DecorationLayerId) -> &[Decoration] {
self.decorations
.get(&layer)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn document_symbols(&self) -> &crate::DocumentOutline {
&self.document_symbols
}
pub fn folding_manager(&self) -> &FoldingManager {
&self.folding_manager
}
pub fn viewport_width(&self) -> usize {
self.viewport_width
}
pub(crate) fn set_view_options(
&mut self,
viewport_width: usize,
wrap_mode: crate::WrapMode,
wrap_indent: crate::WrapIndent,
tab_width: usize,
) {
let viewport_width = viewport_width.max(1);
let tab_width = tab_width.max(1);
let changed = self.viewport_width != viewport_width
|| self.layout_engine.viewport_width() != viewport_width
|| self.layout_engine.wrap_mode() != wrap_mode
|| self.layout_engine.wrap_indent() != wrap_indent
|| self.layout_engine.tab_width() != tab_width;
self.viewport_width = viewport_width;
self.layout_engine.set_viewport_width(viewport_width);
self.layout_engine.set_wrap_mode(wrap_mode);
self.layout_engine.set_wrap_indent(wrap_indent);
self.layout_engine.set_tab_width(tab_width);
if changed {
self.reflow_layout_from_line_index();
}
}
pub(crate) fn insert_style_interval(&mut self, interval: crate::intervals::Interval) {
self.interval_tree.insert(interval);
}
pub(crate) fn remove_style_interval(&mut self, start: usize, end: usize, style_id: StyleId) {
self.interval_tree.remove(start, end, style_id);
}
pub(crate) fn replace_style_layer(
&mut self,
layer: StyleLayerId,
intervals: Vec<crate::intervals::Interval>,
) {
if intervals.is_empty() {
self.style_layers.remove(&layer);
return;
}
let tree = self.style_layers.entry(layer).or_default();
tree.clear();
for interval in intervals {
if interval.start < interval.end {
tree.insert(interval);
}
}
}
pub(crate) fn clear_style_layer(&mut self, layer: StyleLayerId) {
self.style_layers.remove(&layer);
}
pub(crate) fn replace_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
}
pub(crate) fn clear_diagnostics(&mut self) {
self.diagnostics.clear();
}
pub(crate) fn replace_decorations(
&mut self,
layer: DecorationLayerId,
mut decorations: Vec<Decoration>,
) {
decorations.sort_unstable_by_key(|d| (d.range.start, d.range.end));
self.decorations.insert(layer, decorations);
}
pub(crate) fn clear_decorations(&mut self, layer: DecorationLayerId) {
self.decorations.remove(&layer);
}
pub(crate) fn replace_document_symbols(&mut self, symbols: crate::DocumentOutline) {
self.document_symbols = symbols;
}
pub(crate) fn clear_document_symbols(&mut self) {
self.document_symbols = crate::DocumentOutline::default();
}
pub(crate) fn replace_folding_regions(
&mut self,
regions: Vec<FoldRegion>,
preserve_collapsed: bool,
) {
if preserve_collapsed {
self.folding_manager
.replace_derived_regions_preserving_collapsed(regions);
} else {
self.folding_manager.replace_derived_regions(regions);
}
self.invalidate_visual_row_index_cache();
}
pub(crate) fn clear_derived_folding_regions(&mut self) {
self.folding_manager.clear_derived_regions();
self.invalidate_visual_row_index_cache();
}
pub(crate) fn toggle_fold_at_line(&mut self, line: usize) -> bool {
let affected = self
.folding_manager
.regions()
.iter()
.filter(|region| region.start_line == line && region.end_line > region.start_line)
.min_by_key(|region| region.end_line)
.map(|region| (region.start_line, region.end_line));
let toggled = self.folding_manager.toggle_region_starting_at_line(line);
if toggled {
if let Some((start, end)) = affected {
self.sync_visual_row_index_for_logical_range(start, end);
} else {
self.invalidate_visual_row_index_cache();
}
}
toggled
}
pub(crate) fn expand_all_folds(&mut self) {
let had_collapsed = self
.folding_manager
.regions()
.iter()
.any(|region| region.is_collapsed);
self.folding_manager.expand_all();
if had_collapsed {
self.invalidate_visual_row_index_cache();
}
}
pub fn invalidate_visual_row_index_cache(&mut self) {
*self.visual_row_index_cache.borrow_mut() = None;
}
fn visual_row_count_for_logical_line(&self, logical_line: usize) -> usize {
if logical_line >= self.layout_engine.logical_line_count() {
return 0;
}
if Self::is_logical_line_hidden(self.folding_manager.regions(), logical_line) {
return 0;
}
self.layout_engine
.get_line_layout(logical_line)
.map(|layout| layout.visual_line_count)
.unwrap_or(1)
.max(1)
}
fn sync_visual_row_index_for_logical_range(&mut self, start_line: usize, end_line: usize) {
if self.visual_row_index_cache.borrow().is_none() {
return;
}
let line_count = self.layout_engine.logical_line_count();
if line_count == 0 || start_line >= line_count {
return;
}
let end_line = end_line.min(line_count.saturating_sub(1));
let counts = (start_line..=end_line)
.map(|line| (line, self.visual_row_count_for_logical_line(line)))
.collect::<Vec<_>>();
let mut cache = self.visual_row_index_cache.borrow_mut();
let Some(index) = cache.as_mut() else {
return;
};
if index.logical_line_count() != line_count {
*cache = None;
return;
}
for (line, count) in counts {
if !index.set_line_visual_count(line, count) {
*cache = None;
return;
}
}
}
fn sync_visual_row_index_after_text_change(
&mut self,
start_line: usize,
deleted_newlines: usize,
inserted_newlines: usize,
) {
if self.visual_row_index_cache.borrow().is_none() {
return;
}
let line_delta = inserted_newlines as isize - deleted_newlines as isize;
if line_delta != 0 {
let line_count = self.layout_engine.logical_line_count();
let mut cache = self.visual_row_index_cache.borrow_mut();
let Some(index) = cache.as_mut() else {
return;
};
if line_delta > 0 {
let inserted = line_delta as usize;
if index.logical_line_count().saturating_add(inserted) != line_count {
*cache = None;
return;
}
index.insert_lines(
start_line.saturating_add(1),
std::iter::repeat_n(0, inserted),
);
} else {
let removed = (-line_delta) as usize;
if index.logical_line_count().saturating_sub(removed) != line_count
|| !index.remove_lines(start_line.saturating_add(1), removed)
{
*cache = None;
return;
}
}
}
let touch_lines = deleted_newlines.max(inserted_newlines).saturating_add(1);
self.sync_visual_row_index_for_logical_range(
start_line,
start_line.saturating_add(touch_lines),
);
}
pub(crate) fn reflow_layout_from_line_index(&mut self) {
let lines: Vec<String> = (0..self.line_index.line_count())
.map(|line| self.line_index.get_line_text(line).unwrap_or_default())
.collect();
self.layout_engine
.recalculate_all_from_lines(lines.iter().map(String::as_str));
self.invalidate_visual_row_index_cache();
}
fn with_visual_row_index<R>(&self, f: impl FnOnce(&VisualRowIndex) -> R) -> R {
if self.visual_row_index_cache.borrow().is_none() {
let index = self.build_visual_row_index();
*self.visual_row_index_cache.borrow_mut() = Some(index);
}
let cache = self.visual_row_index_cache.borrow();
let index = cache
.as_ref()
.expect("visual-row cache should be initialized");
f(index)
}
fn build_visual_row_index(&self) -> VisualRowIndex {
let counts = (0..self.layout_engine.logical_line_count())
.map(|logical_line| self.visual_row_count_for_logical_line(logical_line))
.collect();
VisualRowIndex::from_line_visual_counts(counts)
}
pub fn visual_line_count(&self) -> usize {
self.with_visual_row_index(|index| index.total_visual_lines())
}
pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
self.with_visual_row_index(|index| {
if index.total_visual_lines() == 0 {
return (0, 0);
}
let clamped_visual = visual_line.min(index.total_visual_lines().saturating_sub(1));
index
.span_for_visual_row(clamped_visual)
.map(|(span, visual_in_logical)| (span.logical_line, visual_in_logical))
.unwrap_or((0, 0))
})
}
pub fn logical_position_to_visual(
&self,
logical_line: usize,
column: usize,
) -> Option<(usize, usize)> {
let regions = self.folding_manager.regions();
let logical_line = Self::closest_visible_line(regions, logical_line)?;
let visual_start = self.visual_start_for_logical_line(logical_line)?;
let tab_width = self.layout_engine.tab_width();
let layout = self.layout_engine.get_line_layout(logical_line)?;
let line_text = self
.line_index
.get_line_text(logical_line)
.unwrap_or_default();
let line_char_len = line_text.chars().count();
let column = column.min(line_char_len);
let mut wrapped_offset = 0usize;
let mut segment_start_col = 0usize;
for wrap_point in &layout.wrap_points {
if column >= wrap_point.char_index {
wrapped_offset = wrapped_offset.saturating_add(1);
segment_start_col = wrap_point.char_index;
} else {
break;
}
}
let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
let mut x_in_line = seg_start_x_in_line;
let mut x_in_segment = 0usize;
for ch in line_text
.chars()
.skip(segment_start_col)
.take(column.saturating_sub(segment_start_col))
{
let w = cell_width_at(ch, x_in_line, tab_width);
x_in_line = x_in_line.saturating_add(w);
x_in_segment = x_in_segment.saturating_add(w);
}
let indent = if wrapped_offset == 0 {
0
} else {
wrap_indent_cells_for_line_text(
&line_text,
self.layout_engine.wrap_indent(),
self.viewport_width,
tab_width,
)
};
Some((
visual_start.saturating_add(wrapped_offset),
indent.saturating_add(x_in_segment),
))
}
pub fn logical_position_to_visual_allow_virtual(
&self,
logical_line: usize,
column: usize,
) -> Option<(usize, usize)> {
let regions = self.folding_manager.regions();
let logical_line = Self::closest_visible_line(regions, logical_line)?;
let visual_start = self.visual_start_for_logical_line(logical_line)?;
let tab_width = self.layout_engine.tab_width();
let layout = self.layout_engine.get_line_layout(logical_line)?;
let line_text = self
.line_index
.get_line_text(logical_line)
.unwrap_or_default();
let line_char_len = line_text.chars().count();
let clamped_column = column.min(line_char_len);
let mut wrapped_offset = 0usize;
let mut segment_start_col = 0usize;
for wrap_point in &layout.wrap_points {
if clamped_column >= wrap_point.char_index {
wrapped_offset = wrapped_offset.saturating_add(1);
segment_start_col = wrap_point.char_index;
} else {
break;
}
}
let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
let mut x_in_line = seg_start_x_in_line;
let mut x_in_segment = 0usize;
for ch in line_text
.chars()
.skip(segment_start_col)
.take(clamped_column.saturating_sub(segment_start_col))
{
let w = cell_width_at(ch, x_in_line, tab_width);
x_in_line = x_in_line.saturating_add(w);
x_in_segment = x_in_segment.saturating_add(w);
}
let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
let indent = if wrapped_offset == 0 {
0
} else {
wrap_indent_cells_for_line_text(
&line_text,
self.layout_engine.wrap_indent(),
self.viewport_width,
tab_width,
)
};
Some((
visual_start.saturating_add(wrapped_offset),
indent.saturating_add(x_in_segment),
))
}
pub fn visual_position_to_logical(
&self,
visual_row: usize,
x_in_cells: usize,
) -> Option<Position> {
let total_visual = self.visual_line_count();
if total_visual == 0 {
return Some(Position::new(0, 0));
}
let clamped_row = visual_row.min(total_visual.saturating_sub(1));
let (logical_line, visual_in_logical) = self.visual_to_logical_line(clamped_row);
let layout = self.layout_engine.get_line_layout(logical_line)?;
let line_text = self
.line_index
.get_line_text(logical_line)
.unwrap_or_default();
let line_char_len = line_text.chars().count();
let segment_start_col = if visual_in_logical == 0 {
0
} else {
layout
.wrap_points
.get(visual_in_logical - 1)
.map(|wp| wp.char_index)
.unwrap_or(0)
};
let segment_end_col = layout
.wrap_points
.get(visual_in_logical)
.map(|wp| wp.char_index)
.unwrap_or(line_char_len)
.max(segment_start_col)
.min(line_char_len);
let tab_width = self.layout_engine.tab_width();
let x_in_cells = if visual_in_logical == 0 {
x_in_cells
} else {
let indent = wrap_indent_cells_for_line_text(
&line_text,
self.layout_engine.wrap_indent(),
self.viewport_width,
tab_width,
);
x_in_cells.saturating_sub(indent)
};
let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
let mut x_in_line = seg_start_x_in_line;
let mut x_in_segment = 0usize;
let mut column = segment_start_col;
for (char_idx, ch) in line_text.chars().enumerate().skip(segment_start_col) {
if char_idx >= segment_end_col {
break;
}
let w = cell_width_at(ch, x_in_line, tab_width);
if x_in_segment.saturating_add(w) > x_in_cells {
break;
}
x_in_line = x_in_line.saturating_add(w);
x_in_segment = x_in_segment.saturating_add(w);
column = column.saturating_add(1);
}
Some(Position::new(logical_line, column))
}
fn visual_start_for_logical_line(&self, logical_line: usize) -> Option<usize> {
if logical_line >= self.layout_engine.logical_line_count() {
return None;
}
self.with_visual_row_index(|index| {
index
.span_for_logical_line(logical_line)
.map(|span| span.start_visual_row)
})
}
fn is_logical_line_hidden(regions: &[FoldRegion], logical_line: usize) -> bool {
regions.iter().any(|region| {
region.is_collapsed
&& logical_line > region.start_line
&& logical_line <= region.end_line
})
}
fn collapsed_region_starting_at(
regions: &[FoldRegion],
start_line: usize,
) -> Option<&FoldRegion> {
regions
.iter()
.filter(|region| {
region.is_collapsed
&& region.start_line == start_line
&& region.end_line > start_line
})
.min_by_key(|region| region.end_line)
}
fn closest_visible_line(regions: &[FoldRegion], logical_line: usize) -> Option<usize> {
let mut line = logical_line;
if regions.is_empty() {
return Some(line);
}
while Self::is_logical_line_hidden(regions, line) {
let Some(start) = regions
.iter()
.filter(|region| {
region.is_collapsed && line > region.start_line && line <= region.end_line
})
.map(|region| region.start_line)
.max()
else {
break;
};
line = start;
}
if Self::is_logical_line_hidden(regions, line) {
None
} else {
Some(line)
}
}
fn fold_right_boundary_bracket_char(&self, region: &FoldRegion) -> Option<char> {
let end_line_text = self.line_index.get_line_text(region.end_line)?;
if let Some(ch) = end_line_text.chars().find(|c| !c.is_whitespace())
&& matches!(ch, '}' | ')' | ']')
{
return Some(ch);
}
for ch in end_line_text.chars().rev() {
if ch.is_whitespace() {
continue;
}
if matches!(ch, '}' | ')' | ']') {
return Some(ch);
}
if matches!(ch, ';' | ',') {
continue;
}
break;
}
None
}
fn styles_at_offset(&self, offset: usize) -> Vec<StyleId> {
let mut styles: Vec<StyleId> = self
.interval_tree
.query_point(offset)
.iter()
.map(|interval| interval.style_id)
.collect();
for tree in self.style_layers.values() {
styles.extend(
tree.query_point(offset)
.iter()
.map(|interval| interval.style_id),
);
}
styles.sort_unstable();
styles.dedup();
styles
}
}
pub struct CommandExecutor {
editor: EditorCore,
command_history: Vec<Command>,
command_history_limit: usize,
undo_redo: UndoRedoManager,
tab_key_behavior: TabKeyBehavior,
indentation_config: IndentationConfig,
auto_pairs: AutoPairsConfig,
snippet_session: Option<SnippetSession>,
line_ending: LineEnding,
preferred_x_cells: Option<usize>,
last_text_delta: Option<TextDelta>,
}
impl CommandExecutor {
pub fn new(text: &str, viewport_width: usize) -> Self {
Self {
editor: EditorCore::new(text, viewport_width),
command_history: Vec::with_capacity(DEFAULT_COMMAND_HISTORY_LIMIT),
command_history_limit: DEFAULT_COMMAND_HISTORY_LIMIT,
undo_redo: UndoRedoManager::new(1000),
tab_key_behavior: TabKeyBehavior::Spaces,
indentation_config: IndentationConfig::default(),
auto_pairs: AutoPairsConfig::default(),
snippet_session: None,
line_ending: LineEnding::detect_in_text(text),
preferred_x_cells: None,
last_text_delta: None,
}
}
pub fn empty(viewport_width: usize) -> Self {
Self::new("", viewport_width)
}
fn update_interval_trees_for_text_edits(&mut self, edits: &[IntervalTextEdit]) {
if edits.is_empty() {
return;
}
self.editor.interval_tree.update_for_text_edits(edits);
for layer_tree in self.editor.style_layers.values_mut() {
layer_tree.update_for_text_edits(edits);
}
}
fn record_command_history(&mut self, command: &Command) {
if self.command_history_limit == 0 {
return;
}
self.command_history.push(command.history_summary());
self.trim_command_history_to_limit();
}
fn trim_command_history_to_limit(&mut self) {
if self.command_history_limit == 0 {
self.command_history.clear();
return;
}
let excess = self
.command_history
.len()
.saturating_sub(self.command_history_limit);
if excess > 0 {
self.command_history.drain(..excess);
}
}
pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
self.last_text_delta = None;
if matches!(
&command,
Command::Cursor(
CursorCommand::SnippetNextPlaceholder | CursorCommand::SnippetPrevPlaceholder
)
) {
} else if matches!(&command, Command::Cursor(_))
|| matches!(
&command,
Command::Edit(
EditCommand::Undo | EditCommand::Redo | EditCommand::ApplyTextEdits { .. }
)
)
{
self.snippet_session = None;
}
self.record_command_history(&command);
let skip_snippet_delta =
matches!(&command, Command::Edit(EditCommand::ApplySnippet { .. }));
if matches!(
command,
Command::Cursor(_) | Command::Edit(EditCommand::Undo | EditCommand::Redo)
) {
self.undo_redo.end_group();
}
let result = match command {
Command::Edit(edit_cmd) => self.execute_edit(edit_cmd),
Command::Cursor(cursor_cmd) => self.execute_cursor(cursor_cmd),
Command::View(view_cmd) => self.execute_view(view_cmd),
Command::Style(style_cmd) => self.execute_style(style_cmd),
}?;
if !skip_snippet_delta
&& let (Some(delta), Some(session)) =
(self.last_text_delta.as_ref(), self.snippet_session.as_mut())
{
session.apply_delta(delta);
}
Ok(result)
}
pub fn last_text_delta(&self) -> Option<&TextDelta> {
self.last_text_delta.as_ref()
}
pub fn take_last_text_delta(&mut self) -> Option<TextDelta> {
self.last_text_delta.take()
}
pub fn execute_batch(
&mut self,
commands: Vec<Command>,
) -> Result<Vec<CommandResult>, CommandError> {
let mut results = Vec::new();
for command in commands {
let result = self.execute(command)?;
results.push(result);
}
Ok(results)
}
pub fn get_command_history(&self) -> &[Command] {
&self.command_history
}
pub fn command_history_limit(&self) -> usize {
self.command_history_limit
}
pub fn set_command_history_limit(&mut self, limit: usize) {
self.command_history_limit = limit;
self.trim_command_history_to_limit();
}
pub fn can_undo(&self) -> bool {
self.undo_redo.can_undo()
}
pub fn can_redo(&self) -> bool {
self.undo_redo.can_redo()
}
pub fn undo_depth(&self) -> usize {
self.undo_redo.undo_depth()
}
pub fn redo_depth(&self) -> usize {
self.undo_redo.redo_depth()
}
pub fn redo_branch_count(&self) -> usize {
self.undo_redo.redo_branch_count()
}
pub fn selected_redo_branch_index(&self) -> Option<usize> {
self.undo_redo.selected_redo_branch_index()
}
pub fn select_redo_branch(&mut self, index: usize) -> Result<(), CommandError> {
self.undo_redo.end_group();
self.undo_redo.select_redo_branch(index)
}
pub fn current_change_group(&self) -> Option<usize> {
self.undo_redo.current_group_id()
}
pub fn undo_coalescing_timeout(&self) -> Duration {
self.undo_redo.coalescing_timeout()
}
pub fn set_undo_coalescing_timeout(&mut self, timeout: Duration) {
self.undo_redo.set_coalescing_timeout(timeout);
}
pub fn is_clean(&self) -> bool {
self.undo_redo.is_clean()
}
pub fn mark_clean(&mut self) {
self.undo_redo.mark_clean();
}
pub fn undo_history_snapshot(&self) -> UndoHistorySnapshot {
self.undo_redo.snapshot()
}
pub fn restore_undo_history(
&mut self,
snapshot: UndoHistorySnapshot,
) -> Result<(), UndoHistoryRestoreError> {
self.last_text_delta = None;
self.undo_redo.restore_from_snapshot(snapshot)
}
pub fn editor(&self) -> &EditorCore {
&self.editor
}
pub fn editor_mut(&mut self) -> &mut EditorCore {
&mut self.editor
}
pub fn tab_key_behavior(&self) -> TabKeyBehavior {
self.tab_key_behavior
}
pub fn set_tab_key_behavior(&mut self, behavior: TabKeyBehavior) {
self.tab_key_behavior = behavior;
}
pub fn indentation_config(&self) -> &IndentationConfig {
&self.indentation_config
}
pub fn set_indentation_config(&mut self, config: IndentationConfig) {
self.indentation_config = config;
}
pub fn auto_pairs_config(&self) -> &AutoPairsConfig {
&self.auto_pairs
}
pub fn set_auto_pairs_config(&mut self, config: AutoPairsConfig) {
self.auto_pairs = config;
}
pub fn set_auto_pairs_enabled(&mut self, enabled: bool) {
self.auto_pairs.enabled = enabled;
}
pub fn has_active_snippet_session(&self) -> bool {
self.snippet_session
.as_ref()
.map(|s| s.is_active())
.unwrap_or(false)
}
pub fn snippet_session(&self) -> Option<&SnippetSession> {
self.snippet_session.as_ref()
}
pub fn set_snippet_session(&mut self, session: Option<SnippetSession>) {
self.snippet_session = session;
}
pub fn preferred_x_cells(&self) -> Option<usize> {
self.preferred_x_cells
}
pub fn set_preferred_x_cells(&mut self, preferred_x_cells: Option<usize>) {
self.preferred_x_cells = preferred_x_cells;
}
pub fn line_ending(&self) -> LineEnding {
self.line_ending
}
pub fn set_line_ending(&mut self, line_ending: LineEnding) {
self.line_ending = line_ending;
}
fn execute_view(&mut self, command: ViewCommand) -> Result<CommandResult, CommandError> {
match command {
ViewCommand::SetViewportWidth { width } => {
if width == 0 {
return Err(CommandError::Other(
"Viewport width must be greater than 0".to_string(),
));
}
self.editor.set_view_options(
width,
self.editor.layout_engine.wrap_mode(),
self.editor.layout_engine.wrap_indent(),
self.editor.layout_engine.tab_width(),
);
Ok(CommandResult::Success)
}
ViewCommand::SetWrapMode { mode } => {
self.editor.set_view_options(
self.editor.viewport_width,
mode,
self.editor.layout_engine.wrap_indent(),
self.editor.layout_engine.tab_width(),
);
Ok(CommandResult::Success)
}
ViewCommand::SetWrapIndent { indent } => {
self.editor.set_view_options(
self.editor.viewport_width,
self.editor.layout_engine.wrap_mode(),
indent,
self.editor.layout_engine.tab_width(),
);
Ok(CommandResult::Success)
}
ViewCommand::SetTabWidth { width } => {
if width == 0 {
return Err(CommandError::Other(
"Tab width must be greater than 0".to_string(),
));
}
self.editor.set_view_options(
self.editor.viewport_width,
self.editor.layout_engine.wrap_mode(),
self.editor.layout_engine.wrap_indent(),
width,
);
Ok(CommandResult::Success)
}
ViewCommand::SetTabKeyBehavior { behavior } => {
self.tab_key_behavior = behavior;
Ok(CommandResult::Success)
}
ViewCommand::SetIndentationConfig { config } => {
self.indentation_config = config;
Ok(CommandResult::Success)
}
ViewCommand::SetAutoPairsConfig { config } => {
self.set_auto_pairs_config(config);
Ok(CommandResult::Success)
}
ViewCommand::SetAutoPairsEnabled { enabled } => {
self.set_auto_pairs_enabled(enabled);
Ok(CommandResult::Success)
}
ViewCommand::SetWordBoundaryAsciiBoundaryChars { boundary_chars } => {
self.editor
.set_word_boundary_ascii_boundary_chars(&boundary_chars);
Ok(CommandResult::Success)
}
ViewCommand::ResetWordBoundaryDefaults => {
self.editor.reset_word_boundary_defaults();
Ok(CommandResult::Success)
}
ViewCommand::ScrollTo { line } => {
if line >= self.editor.line_index.line_count() {
return Err(CommandError::InvalidPosition { line, column: 0 });
}
Ok(CommandResult::Success)
}
ViewCommand::GetViewport { start_row, count } => {
let grid = self.editor.get_headless_grid_styled(start_row, count);
Ok(CommandResult::Viewport(grid))
}
}
}
fn execute_style(&mut self, command: StyleCommand) -> Result<CommandResult, CommandError> {
match command {
StyleCommand::AddStyle {
start,
end,
style_id,
} => {
if start >= end {
return Err(CommandError::InvalidRange { start, end });
}
let interval = crate::intervals::Interval::new(start, end, style_id);
self.editor.insert_style_interval(interval);
Ok(CommandResult::Success)
}
StyleCommand::RemoveStyle {
start,
end,
style_id,
} => {
self.editor.remove_style_interval(start, end, style_id);
Ok(CommandResult::Success)
}
StyleCommand::Fold {
start_line,
end_line,
} => {
if start_line >= end_line {
return Err(CommandError::InvalidRange {
start: start_line,
end: end_line,
});
}
let mut region = crate::intervals::FoldRegion::new(start_line, end_line);
region.collapse();
self.editor.folding_manager.add_region(region);
self.editor
.sync_visual_row_index_for_logical_range(start_line, end_line);
Ok(CommandResult::Success)
}
StyleCommand::Unfold { start_line } => {
let affected = self
.editor
.folding_manager
.innermost_region_bounds_for_line(start_line);
self.editor.folding_manager.expand_line(start_line);
if let Some((start, end)) = affected {
self.editor
.sync_visual_row_index_for_logical_range(start, end);
}
Ok(CommandResult::Success)
}
StyleCommand::UnfoldAll => {
let affected = self
.editor
.folding_manager
.regions()
.iter()
.filter(|region| region.is_collapsed)
.fold(None::<(usize, usize)>, |acc, region| match acc {
Some((start, end)) => {
Some((start.min(region.start_line), end.max(region.end_line)))
}
None => Some((region.start_line, region.end_line)),
});
self.editor.folding_manager.expand_all();
if let Some((start, end)) = affected {
self.editor
.sync_visual_row_index_for_logical_range(start, end);
}
Ok(CommandResult::Success)
}
StyleCommand::UpdateBracketMatchHighlights => {
self.execute_update_bracket_match_highlights_command()
}
StyleCommand::ClearBracketMatchHighlights => {
self.execute_clear_bracket_match_highlights_command()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edit_insert() {
let mut executor = CommandExecutor::new("Hello", 80);
let result = executor.execute(Command::Edit(EditCommand::Insert {
offset: 5,
text: " World".to_string(),
}));
assert!(result.is_ok());
assert_eq!(executor.editor().get_text(), "Hello World");
}
#[test]
fn test_edit_delete() {
let mut executor = CommandExecutor::new("Hello World", 80);
let result = executor.execute(Command::Edit(EditCommand::Delete {
start: 5,
length: 6,
}));
assert!(result.is_ok());
assert_eq!(executor.editor().get_text(), "Hello");
}
#[test]
fn test_edit_replace() {
let mut executor = CommandExecutor::new("Hello World", 80);
let result = executor.execute(Command::Edit(EditCommand::Replace {
start: 6,
length: 5,
text: "Rust".to_string(),
}));
assert!(result.is_ok());
assert_eq!(executor.editor().get_text(), "Hello Rust");
}
#[test]
fn test_cursor_move_to() {
let mut executor = CommandExecutor::new("Line 1\nLine 2\nLine 3", 80);
let result = executor.execute(Command::Cursor(CursorCommand::MoveTo {
line: 1,
column: 3,
}));
assert!(result.is_ok());
assert_eq!(executor.editor().cursor_position(), Position::new(1, 3));
}
#[test]
fn test_cursor_selection() {
let mut executor = CommandExecutor::new("Hello World", 80);
let result = executor.execute(Command::Cursor(CursorCommand::SetSelection {
start: Position::new(0, 0),
end: Position::new(0, 5),
}));
assert!(result.is_ok());
assert!(executor.editor().selection().is_some());
}
#[test]
fn test_view_set_width() {
let mut executor = CommandExecutor::new("Test", 80);
let result = executor.execute(Command::View(ViewCommand::SetViewportWidth { width: 40 }));
assert!(result.is_ok());
assert_eq!(executor.editor().viewport_width(), 40);
}
#[test]
fn test_style_add_remove() {
let mut executor = CommandExecutor::new("Hello World", 80);
let result = executor.execute(Command::Style(StyleCommand::AddStyle {
start: 0,
end: 5,
style_id: 1,
}));
assert!(result.is_ok());
let result = executor.execute(Command::Style(StyleCommand::RemoveStyle {
start: 0,
end: 5,
style_id: 1,
}));
assert!(result.is_ok());
}
#[test]
fn test_batch_execution() {
let mut executor = CommandExecutor::new("", 80);
let commands = vec![
Command::Edit(EditCommand::Insert {
offset: 0,
text: "Hello".to_string(),
}),
Command::Edit(EditCommand::Insert {
offset: 5,
text: " World".to_string(),
}),
];
let results = executor.execute_batch(commands);
assert!(results.is_ok());
assert_eq!(executor.editor().get_text(), "Hello World");
}
#[test]
fn test_error_invalid_offset() {
let mut executor = CommandExecutor::new("Hello", 80);
let result = executor.execute(Command::Edit(EditCommand::Insert {
offset: 100,
text: "X".to_string(),
}));
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CommandError::InvalidOffset(_)
));
}
}