use super::*;
impl CommandExecutor {
pub(super) fn execute_edit(
&mut self,
command: EditCommand,
) -> Result<CommandResult, CommandError> {
match command {
EditCommand::Undo => self.execute_undo_command(),
EditCommand::Redo => self.execute_redo_command(),
EditCommand::EndUndoGroup => {
self.undo_redo.end_group();
Ok(CommandResult::Success)
}
EditCommand::ReplaceCurrent {
query,
replacement,
options,
} => self.execute_replace_current_command(query, replacement, options),
EditCommand::ReplaceAll {
query,
replacement,
options,
} => self.execute_replace_all_command(query, replacement, options),
EditCommand::DeleteToPrevTabStop => self.execute_delete_to_prev_tab_stop_command(),
EditCommand::DeleteGraphemeBack => {
self.execute_delete_by_boundary_command(false, TextBoundary::Grapheme)
}
EditCommand::DeleteGraphemeForward => {
self.execute_delete_by_boundary_command(true, TextBoundary::Grapheme)
}
EditCommand::DeleteWordBack => {
self.execute_delete_by_boundary_command(false, TextBoundary::Word)
}
EditCommand::DeleteWordForward => {
self.execute_delete_by_boundary_command(true, TextBoundary::Word)
}
EditCommand::Backspace => self.execute_backspace_command(),
EditCommand::DeleteForward => self.execute_delete_forward_command(),
EditCommand::InsertText { text } => self.execute_insert_text_command(text),
EditCommand::TypeChar { ch } => self.execute_type_char_command(ch),
EditCommand::InsertTab => self.execute_insert_tab_command(),
EditCommand::InsertNewline { auto_indent } => {
self.execute_insert_newline_command(auto_indent)
}
EditCommand::Indent => self.execute_indent_command(false),
EditCommand::Outdent => self.execute_indent_command(true),
EditCommand::DuplicateLines => self.execute_duplicate_lines_command(),
EditCommand::DeleteLines => self.execute_delete_lines_command(),
EditCommand::MoveLinesUp => self.execute_move_lines_command(true),
EditCommand::MoveLinesDown => self.execute_move_lines_command(false),
EditCommand::JoinLines => self.execute_join_lines_command(),
EditCommand::SplitLine => self.execute_insert_newline_command(false),
EditCommand::ToggleComment { config } => self.execute_toggle_comment_command(config),
EditCommand::ApplyTextEdits { edits } => self.execute_apply_text_edits_command(edits),
EditCommand::ApplySnippet {
start,
end,
snippet,
additional_edits,
} => self.execute_apply_snippet_command(start, end, snippet, additional_edits),
EditCommand::Insert { offset, text } => self.execute_insert_command(offset, text),
EditCommand::Delete { start, length } => self.execute_delete_command(start, length),
EditCommand::Replace {
start,
length,
text,
} => self.execute_replace_command(start, length, text, false, None),
EditCommand::ReplaceCoalescingUndo {
start,
length,
text,
} => self.execute_replace_command(start, length, text, true, None),
EditCommand::ReplaceCoalescingUndoWithSelection {
start,
length,
text,
selection_start,
selection_end,
} => self.execute_replace_command(
start,
length,
text,
true,
Some((selection_start, selection_end)),
),
}
}
pub(super) fn execute_undo_command(&mut self) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_char_count = self.editor.char_count();
let steps = self
.undo_redo
.pop_undo_group()?
.ok_or_else(|| CommandError::Other("Nothing to undo".to_string()))?;
let undo_group_id = steps.first().map(|s| s.group_id);
let mut delta_edits: Vec<TextDeltaEdit> = Vec::new();
for step in &steps {
let mut step_edits: Vec<TextDeltaEdit> = step
.edits
.iter()
.map(|edit| TextDeltaEdit {
start: edit.start_after,
deleted_text: edit.inserted_text.clone(),
inserted_text: edit.deleted_text.clone(),
})
.collect();
step_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
delta_edits.extend(step_edits);
self.apply_undo_edits(&step.edits)?;
self.restore_selection_set(step.before_selection.clone());
}
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id,
});
Ok(CommandResult::Success)
}
pub(super) fn execute_redo_command(&mut self) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_char_count = self.editor.char_count();
let steps = self
.undo_redo
.pop_redo_group()?
.ok_or_else(|| CommandError::Other("Nothing to redo".to_string()))?;
let undo_group_id = steps.first().map(|s| s.group_id);
let mut delta_edits: Vec<TextDeltaEdit> = Vec::new();
for step in &steps {
let mut step_edits: Vec<TextDeltaEdit> = step
.edits
.iter()
.map(|edit| TextDeltaEdit {
start: edit.start_before,
deleted_text: edit.deleted_text.clone(),
inserted_text: edit.inserted_text.clone(),
})
.collect();
step_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
delta_edits.extend(step_edits);
self.apply_redo_edits(&step.edits)?;
self.restore_selection_set(step.after_selection.clone());
}
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id,
});
Ok(CommandResult::Success)
}
pub(super) fn execute_insert_text_command(
&mut self,
text: String,
) -> Result<CommandResult, CommandError> {
if text.is_empty() {
let primary_non_empty = self
.editor
.selection
.as_ref()
.is_some_and(|s| s.start != s.end);
let secondary_non_empty = self
.editor
.secondary_selections
.iter()
.any(|s| s.start != s.end);
if !primary_non_empty && !secondary_non_empty {
return Ok(CommandResult::Success);
}
}
let text = crate::text::normalize_crlf_to_lf_string(text);
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let mut selections: Vec<Selection> =
Vec::with_capacity(1 + self.editor.secondary_selections.len());
let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
start: self.editor.cursor_position,
end: self.editor.cursor_position,
direction: SelectionDirection::Forward,
});
selections.push(primary_selection);
selections.extend(self.editor.secondary_selections.iter().cloned());
let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
let text_char_len = text.chars().count();
struct Op {
selection_index: usize,
start_offset: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
insert_text: String,
insert_char_len: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
(selection.start, selection.end)
} else {
(selection.end, selection.start)
};
let (start_offset, start_pad) =
self.position_to_char_offset_and_virtual_pad(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
let delete_len = end_offset.saturating_sub(start_offset);
let insert_char_len = start_pad + text_char_len;
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
let mut insert_text = String::with_capacity(text.len() + start_pad);
for _ in 0..start_pad {
insert_text.push(' ');
}
insert_text.push_str(&text);
ops.push(Op {
selection_index,
start_offset,
start_after: start_offset,
delete_len,
deleted_text,
insert_text,
insert_char_len,
});
}
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
delta += op.insert_char_len as i64 - op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, op.insert_char_len)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
&op.insert_text,
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
for offset in &caret_offsets {
let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == new_primary_index {
None
} else {
Some(sel)
}
})
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.insert_text,
})
.collect();
let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
let coalescible_insert = is_pure_insert && !text.contains('\n');
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, coalescible_insert);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_type_char_command(
&mut self,
ch: char,
) -> Result<CommandResult, CommandError> {
if !self.auto_pairs.enabled {
return self.execute_insert_text_command(ch.to_string());
}
let ch = if ch == '\r' { '\n' } else { ch };
if ch == '\n' {
return self.execute_insert_newline_command(true);
}
if ch == '\t' {
return self.execute_insert_tab_command();
}
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let selections = before_selection.selections.clone();
let primary_index = before_selection.primary_index;
#[derive(Debug, Clone)]
enum AfterSel {
Caret {
delta_from_start: usize,
},
Range {
start_delta: usize,
end_delta: usize,
},
}
#[derive(Debug)]
struct Op {
selection_index: usize,
start_offset: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
insert_text: String,
insert_char_len: usize,
apply: bool,
after: AfterSel,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
let doc_char_count = self.editor.char_count();
let next_char_matches = |this: &Self, offset: usize, ch: char| -> bool {
this.editor.line_index.char_at(offset) == Some(ch)
};
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
(selection.start, selection.end)
} else {
(selection.end, selection.start)
};
let (start_offset, start_pad) =
self.position_to_char_offset_and_virtual_pad(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
let delete_len = end_offset.saturating_sub(start_offset);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
if self.auto_pairs.skip_over_closing
&& delete_len == 0
&& start_pad == 0
&& self.auto_pairs.open_for_close(ch).is_some()
&& next_char_matches(self, start_offset, ch)
{
ops.push(Op {
selection_index,
start_offset,
start_after: start_offset,
delete_len: 0,
deleted_text: String::new(),
insert_text: String::new(),
insert_char_len: 0,
apply: false,
after: AfterSel::Caret {
delta_from_start: 1,
},
});
continue;
}
let mut insert_text = String::new();
for _ in 0..start_pad {
insert_text.push(' ');
}
let mut apply = true;
let after = if let Some(close) = self.auto_pairs.close_for_open(ch) {
if delete_len > 0 && self.auto_pairs.wrap_selection {
insert_text.push(ch);
insert_text.push_str(&deleted_text);
insert_text.push(close);
AfterSel::Range {
start_delta: start_pad + 1,
end_delta: start_pad + 1 + delete_len,
}
} else if delete_len == 0 {
insert_text.push(ch);
insert_text.push(close);
AfterSel::Caret {
delta_from_start: start_pad + 1,
}
} else {
insert_text.push(ch);
AfterSel::Caret {
delta_from_start: start_pad + 1,
}
}
} else {
insert_text.push(ch);
AfterSel::Caret {
delta_from_start: start_pad + 1,
}
};
let insert_char_len = insert_text.chars().count();
if insert_char_len == 0 && delete_len == 0 {
apply = false;
}
ops.push(Op {
selection_index,
start_offset,
start_after: start_offset,
delete_len,
deleted_text,
insert_text,
insert_char_len,
apply,
after,
});
}
if !ops
.iter()
.any(|op| op.apply && (op.delete_len > 0 || !op.insert_text.is_empty()))
{
let mut new_selections: Vec<Selection> = Vec::with_capacity(ops.len());
for op in &ops {
let offset = match op.after {
AfterSel::Caret { delta_from_start } => op.start_offset + delta_from_start,
AfterSel::Range { .. } => op.start_offset,
}
.min(doc_char_count);
let (line, column) = self.editor.line_index.char_offset_to_position(offset);
let pos = Position::new(line, column);
new_selections.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_selections, new_primary_index) =
crate::selection_set::normalize_selections(new_selections, primary_index);
let primary = new_selections
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_selections
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| (idx != new_primary_index).then_some(sel))
.collect();
return Ok(CommandResult::Success);
}
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
if op.apply {
delta += op.insert_char_len as i64 - op.delete_len as i64;
}
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.filter_map(|&idx| {
let op = &ops[idx];
op.apply.then_some(IntervalTextEdit::new(
op.start_offset,
op.delete_len,
op.insert_char_len,
))
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
if !op.apply {
continue;
}
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
&op.insert_text,
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_selections: Vec<Selection> = vec![
Selection {
start: Position::new(0, 0),
end: Position::new(0, 0),
direction: SelectionDirection::Forward,
};
ops.len()
];
for op in &ops {
let base = op.start_after;
let (start_offset, end_offset, is_range) = match op.after {
AfterSel::Caret { delta_from_start } => {
let caret = base.saturating_add(delta_from_start);
(caret, caret, false)
}
AfterSel::Range {
start_delta,
end_delta,
} => (
base.saturating_add(start_delta),
base.saturating_add(end_delta),
true,
),
};
let (start_line, start_col) = self
.editor
.line_index
.char_offset_to_position(start_offset.min(self.editor.char_count()));
let (end_line, end_col) = self
.editor
.line_index
.char_offset_to_position(end_offset.min(self.editor.char_count()));
let start_pos = Position::new(start_line, start_col);
let end_pos = Position::new(end_line, end_col);
new_selections[op.selection_index] = Selection {
start: start_pos,
end: end_pos,
direction: if is_range && start_pos > end_pos {
SelectionDirection::Backward
} else {
SelectionDirection::Forward
},
};
}
let (new_selections, new_primary_index) =
crate::selection_set::normalize_selections(new_selections, primary_index);
let primary = new_selections
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary selection".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = if primary.start == primary.end {
None
} else {
Some(primary.clone())
};
self.editor.secondary_selections = new_selections
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| (idx != new_primary_index).then_some(sel))
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.filter(|op| op.apply)
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.insert_text,
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
let coalescible_insert = is_pure_insert && ch != '\n';
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, coalescible_insert);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn bracket_pair_for_char(bracket: char) -> Option<(char, char, bool)> {
match bracket {
'(' => Some(('(', ')', true)),
')' => Some(('(', ')', false)),
'[' => Some(('[', ']', true)),
']' => Some(('[', ']', false)),
'{' => Some(('{', '}', true)),
'}' => Some(('{', '}', false)),
_ => None,
}
}
pub(super) fn bracket_at_caret(
&self,
caret_offset: usize,
) -> Option<(usize, char, char, bool)> {
let doc_char_count = self.editor.line_index.char_count();
if caret_offset > 0
&& let Some(ch) = self.editor.line_index.char_at(caret_offset - 1)
&& let Some((open, close, is_open)) = Self::bracket_pair_for_char(ch)
{
return Some((caret_offset - 1, open, close, is_open));
}
if caret_offset < doc_char_count
&& let Some(ch) = self.editor.line_index.char_at(caret_offset)
&& let Some((open, close, is_open)) = Self::bracket_pair_for_char(ch)
{
return Some((caret_offset, open, close, is_open));
}
None
}
pub(super) fn matching_bracket_offset(
&self,
bracket_offset: usize,
open: char,
close: char,
is_open: bool,
) -> Option<usize> {
let doc_char_count = self.editor.line_index.char_count();
if doc_char_count == 0 {
return None;
}
if is_open {
let mut nesting = 0usize;
let mut offset = bracket_offset.saturating_add(1);
while offset < doc_char_count {
let ch = self.editor.line_index.char_at(offset)?;
if ch == open {
nesting = nesting.saturating_add(1);
} else if ch == close {
if nesting == 0 {
return Some(offset);
}
nesting = nesting.saturating_sub(1);
}
offset = offset.saturating_add(1);
}
None
} else {
if bracket_offset == 0 {
return None;
}
let mut nesting = 0usize;
let mut offset = bracket_offset;
while offset > 0 {
offset = offset.saturating_sub(1);
let ch = self.editor.line_index.char_at(offset)?;
if ch == close {
nesting = nesting.saturating_add(1);
} else if ch == open {
if nesting == 0 {
return Some(offset);
}
nesting = nesting.saturating_sub(1);
}
}
None
}
}
pub(super) fn execute_move_to_matching_bracket_command(
&mut self,
) -> Result<CommandResult, CommandError> {
let before_selection = self.snapshot_selection_set();
let selections = before_selection.selections.clone();
let primary_index = before_selection.primary_index;
let doc_char_count = self.editor.char_count();
let mut new_carets: Vec<Selection> = Vec::with_capacity(selections.len());
for selection in &selections {
let caret_offset = self.position_to_char_offset_clamped(selection.end);
let target_offset = self
.bracket_at_caret(caret_offset)
.and_then(|(bracket_offset, open, close, is_open)| {
self.matching_bracket_offset(bracket_offset, open, close, is_open)
})
.unwrap_or(caret_offset)
.min(doc_char_count);
let (line, column) = self
.editor
.line_index
.char_offset_to_position(target_offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.preferred_x_cells = self
.editor
.logical_position_to_visual(primary.end.line, primary.end.column)
.map(|(_, x)| x);
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| (idx != new_primary_index).then_some(sel))
.collect();
Ok(CommandResult::Success)
}
pub(super) fn execute_snippet_navigation_command(
&mut self,
forward: bool,
) -> Result<CommandResult, CommandError> {
let Some(session) = self.snippet_session.as_mut() else {
return Ok(CommandResult::Success);
};
let action = if forward {
session.next()
} else {
session.prev()
};
match action {
SnippetNavigation::Noop => Ok(CommandResult::Success),
SnippetNavigation::Finish(offset) => {
let doc_char_count = self.editor.char_count();
let target = offset.min(doc_char_count);
let (line, column) = self.editor.line_index.char_offset_to_position(target);
let pos = Position::new(line, column);
self.editor.cursor_position = pos;
self.editor.selection = None;
self.editor.secondary_selections.clear();
self.preferred_x_cells = self
.editor
.logical_position_to_visual(pos.line, pos.column)
.map(|(_, x)| x);
self.snippet_session = None;
Ok(CommandResult::Success)
}
SnippetNavigation::SelectRanges(ranges) => {
if ranges.is_empty() {
return Ok(CommandResult::Success);
}
let doc_char_count = self.editor.char_count();
let mut selections: Vec<Selection> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
let a = start.min(doc_char_count);
let b = end.min(doc_char_count);
let (s_line, s_col) = self.editor.line_index.char_offset_to_position(a);
let (e_line, e_col) = self.editor.line_index.char_offset_to_position(b);
selections.push(Selection {
start: Position::new(s_line, s_col),
end: Position::new(e_line, e_col),
direction: SelectionDirection::Forward,
});
}
let (selections, primary_index) =
crate::selection_set::normalize_selections(selections, 0);
self.restore_selection_set(SelectionSetSnapshot {
selections,
primary_index,
});
let pos = self.editor.cursor_position;
self.preferred_x_cells = self
.editor
.logical_position_to_visual(pos.line, pos.column)
.map(|(_, x)| x);
Ok(CommandResult::Success)
}
}
}
pub(super) fn execute_update_bracket_match_highlights_command(
&mut self,
) -> Result<CommandResult, CommandError> {
let snapshot = self.snapshot_selection_set();
let selections = snapshot.selections;
let mut intervals: Vec<(usize, usize, StyleId)> =
Vec::with_capacity(selections.len().saturating_mul(2));
for selection in &selections {
let caret_offset = self.position_to_char_offset_clamped(selection.end);
let Some((bracket_offset, open, close, is_open)) = self.bracket_at_caret(caret_offset)
else {
continue;
};
let Some(other_offset) =
self.matching_bracket_offset(bracket_offset, open, close, is_open)
else {
continue;
};
intervals.push((
bracket_offset,
bracket_offset.saturating_add(1),
crate::MATCH_HIGHLIGHT_STYLE_ID,
));
intervals.push((
other_offset,
other_offset.saturating_add(1),
crate::MATCH_HIGHLIGHT_STYLE_ID,
));
}
intervals.sort_unstable();
intervals.dedup();
if intervals.is_empty() {
self.editor
.style_layers
.remove(&StyleLayerId::BRACKET_MATCHES);
return Ok(CommandResult::Success);
}
let tree = self
.editor
.style_layers
.entry(StyleLayerId::BRACKET_MATCHES)
.or_default();
tree.clear();
for (start, end, style_id) in intervals {
if start < end {
tree.insert(crate::intervals::Interval::new(start, end, style_id));
}
}
Ok(CommandResult::Success)
}
pub(super) fn execute_clear_bracket_match_highlights_command(
&mut self,
) -> Result<CommandResult, CommandError> {
self.editor
.style_layers
.remove(&StyleLayerId::BRACKET_MATCHES);
Ok(CommandResult::Success)
}
pub(super) fn execute_insert_tab_command(&mut self) -> Result<CommandResult, CommandError> {
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let mut selections: Vec<Selection> =
Vec::with_capacity(1 + self.editor.secondary_selections.len());
let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
start: self.editor.cursor_position,
end: self.editor.cursor_position,
direction: SelectionDirection::Forward,
});
selections.push(primary_selection);
selections.extend(self.editor.secondary_selections.iter().cloned());
let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
let should_indent_lines = selections.iter().any(|selection| {
if selection.start == selection.end {
return false;
}
let (min_pos, max_pos) = crate::selection_set::selection_min_max(selection);
if min_pos.line != max_pos.line {
return true;
}
if min_pos.column != 0 {
return false;
}
let line_text = self
.editor
.line_index
.get_line_text(min_pos.line)
.unwrap_or_default();
let line_len = line_text.chars().count();
max_pos.column >= line_len
});
if should_indent_lines {
return self.execute_indent_command(false);
}
let tab_width = self.editor.layout_engine.tab_width();
struct Op {
selection_index: usize,
start_offset: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
insert_text: String,
insert_char_len: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
(selection.start, selection.end)
} else {
(selection.end, selection.start)
};
let (start_offset, start_pad) =
self.position_to_char_offset_and_virtual_pad(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
let delete_len = end_offset.saturating_sub(start_offset);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
let line_text = self
.editor
.line_index
.get_line_text(range_start_pos.line)
.unwrap_or_default();
let line_char_len = line_text.chars().count();
let clamped_col = range_start_pos.column.min(line_char_len);
let x_in_line =
visual_x_for_column(&line_text, clamped_col, tab_width).saturating_add(start_pad);
let mut insert_text = String::new();
for _ in 0..start_pad {
insert_text.push(' ');
}
match self.tab_key_behavior {
TabKeyBehavior::Tab => {
insert_text.push('\t');
ops.push(Op {
selection_index,
start_offset,
start_after: start_offset,
delete_len,
deleted_text,
insert_text,
insert_char_len: start_pad + 1,
});
}
TabKeyBehavior::Spaces => {
let tab_width = tab_width.max(1);
let rem = x_in_line % tab_width;
let spaces = tab_width - rem;
for _ in 0..spaces {
insert_text.push(' ');
}
ops.push(Op {
selection_index,
start_offset,
start_after: start_offset,
delete_len,
deleted_text,
insert_text,
insert_char_len: start_pad + spaces,
});
}
}
}
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
delta += op.insert_char_len as i64 - op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, op.insert_char_len)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
&op.insert_text,
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
for offset in &caret_offsets {
let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == new_primary_index {
None
} else {
Some(sel)
}
})
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.insert_text,
})
.collect();
let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
let coalescible_insert = is_pure_insert;
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, coalescible_insert);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn leading_whitespace_prefix(line_text: &str) -> String {
line_text
.chars()
.take_while(|ch| *ch == ' ' || *ch == '\t')
.collect()
}
pub(super) fn split_at_char(s: &str, char_idx: usize) -> (&str, &str) {
let byte_idx = s
.char_indices()
.nth(char_idx)
.map(|(b, _)| b)
.unwrap_or_else(|| s.len());
s.split_at(byte_idx)
}
pub(super) fn last_non_space_or_tab(s: &str) -> Option<char> {
s.chars().rev().find(|ch| *ch != ' ' && *ch != '\t')
}
pub(super) fn first_non_space_or_tab(s: &str) -> Option<char> {
s.chars().find(|ch| *ch != ' ' && *ch != '\t')
}
pub(super) fn matching_closer_for(open: char) -> Option<char> {
match open {
'{' => Some('}'),
'[' => Some(']'),
'(' => Some(')'),
_ => None,
}
}
pub(super) fn indent_unit_for_newline(&self) -> String {
match self.indentation_config.style {
IndentStyle::Tabs => "\t".to_string(),
IndentStyle::Spaces(width) => " ".repeat(width.max(1) as usize),
}
}
pub(super) fn indent_unit(&self) -> String {
match self.tab_key_behavior {
TabKeyBehavior::Tab => "\t".to_string(),
TabKeyBehavior::Spaces => " ".repeat(self.editor.layout_engine.tab_width().max(1)),
}
}
pub(super) fn execute_insert_newline_command(
&mut self,
auto_indent: bool,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let mut selections: Vec<Selection> =
Vec::with_capacity(1 + self.editor.secondary_selections.len());
let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
start: self.editor.cursor_position,
end: self.editor.cursor_position,
direction: SelectionDirection::Forward,
});
selections.push(primary_selection);
selections.extend(self.editor.secondary_selections.iter().cloned());
let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
struct Op {
selection_index: usize,
start_offset: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
insert_text: String,
insert_char_len: usize,
caret_offset_in_insert: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) =
crate::selection_set::selection_min_max(selection);
let start_offset = self.position_to_char_offset_clamped(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
let delete_len = end_offset.saturating_sub(start_offset);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
let (insert_text, caret_offset_in_insert) = if auto_indent {
let line_text = self
.editor
.line_index
.get_line_text(range_start_pos.line)
.unwrap_or_default();
let base_indent = Self::leading_whitespace_prefix(&line_text);
let indent_unit = self.indent_unit_for_newline();
let (before_cursor, after_cursor) =
Self::split_at_char(&line_text, range_start_pos.column);
let before_last = Self::last_non_space_or_tab(before_cursor);
let after_first = Self::first_non_space_or_tab(after_cursor);
if let Some(open) = before_last
&& self.indentation_config.indent_triggers.contains(&open)
&& Self::matching_closer_for(open)
.is_some_and(|close| after_first == Some(close))
{
let inner_indent = format!("{base_indent}{indent_unit}");
let insert = format!("\n{inner_indent}\n{base_indent}");
let caret = 1 + inner_indent.chars().count();
(insert, caret)
} else {
let mut indent = base_indent;
if let Some(last) = before_last
&& self.indentation_config.indent_triggers.contains(&last)
{
indent.push_str(&indent_unit);
}
let insert = format!("\n{indent}");
let caret = insert.chars().count();
(insert, caret)
}
} else {
("\n".to_string(), 1)
};
let insert_char_len = insert_text.chars().count();
ops.push(Op {
selection_index,
start_offset,
start_after: start_offset,
delete_len,
deleted_text,
insert_text,
insert_char_len,
caret_offset_in_insert,
});
}
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
caret_offsets[op.selection_index] = effective_start + op.caret_offset_in_insert;
delta += op.insert_char_len as i64 - op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, op.insert_char_len)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
&op.insert_text,
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
for offset in &caret_offsets {
let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == new_primary_index {
None
} else {
Some(sel)
}
})
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.insert_text,
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_indent_command(
&mut self,
outdent: bool,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let selections = before_selection.selections.clone();
let mut lines: Vec<usize> = Vec::new();
for sel in &selections {
let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
let end_line = if min_pos.line < max_pos.line && max_pos.column == 0 {
max_pos.line.saturating_sub(1)
} else {
max_pos.line
};
for line in min_pos.line..=end_line {
lines.push(line);
}
}
lines.sort_unstable();
lines.dedup();
if lines.is_empty() {
return Ok(CommandResult::Success);
}
let tab_width = self.editor.layout_engine.tab_width().max(1);
let indent_unit = self.indent_unit();
let indent_chars = indent_unit.chars().count();
#[derive(Debug)]
struct Op {
start_offset: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
insert_text: String,
insert_len: usize,
}
let mut ops: Vec<Op> = Vec::new();
let mut line_deltas: std::collections::HashMap<usize, isize> =
std::collections::HashMap::new();
for line in lines {
if line >= self.editor.line_index.line_count() {
continue;
}
let start_offset = self.editor.line_index.position_to_char_offset(line, 0);
let line_text = self
.editor
.line_index
.get_line_text(line)
.unwrap_or_default();
if outdent {
let mut remove_len = 0usize;
if let Some(first) = line_text.chars().next() {
if first == '\t' {
remove_len = 1;
} else if first == ' ' {
let leading_spaces = line_text.chars().take_while(|c| *c == ' ').count();
remove_len = leading_spaces.min(tab_width);
}
}
if remove_len == 0 {
continue;
}
let deleted_text = self.editor.text_range(start_offset, remove_len);
ops.push(Op {
start_offset,
start_after: start_offset,
delete_len: remove_len,
deleted_text,
insert_text: String::new(),
insert_len: 0,
});
line_deltas.insert(line, -(remove_len as isize));
} else {
if indent_chars == 0 {
continue;
}
ops.push(Op {
start_offset,
start_after: start_offset,
delete_len: 0,
deleted_text: String::new(),
insert_text: indent_unit.clone(),
insert_len: indent_chars,
});
line_deltas.insert(line, indent_chars as isize);
}
}
if ops.is_empty() {
return Ok(CommandResult::Success);
}
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
delta += op.insert_len as i64 - op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, op.insert_len)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
for &idx in &desc_indices {
let op = &ops[idx];
self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
&op.insert_text,
);
}
let line_index = &self.editor.line_index;
let apply_delta = |pos: &mut Position, deltas: &std::collections::HashMap<usize, isize>| {
let Some(delta) = deltas.get(&pos.line) else {
return;
};
let new_col = if *delta >= 0 {
pos.column.saturating_add(*delta as usize)
} else {
pos.column.saturating_sub((-*delta) as usize)
};
pos.column = Self::clamp_column_for_line_with_index(line_index, pos.line, new_col);
};
apply_delta(&mut self.editor.cursor_position, &line_deltas);
if let Some(sel) = &mut self.editor.selection {
apply_delta(&mut sel.start, &line_deltas);
apply_delta(&mut sel.end, &line_deltas);
}
for sel in &mut self.editor.secondary_selections {
apply_delta(&mut sel.start, &line_deltas);
apply_delta(&mut sel.end, &line_deltas);
}
self.normalize_cursor_and_selection();
self.preferred_x_cells = self
.editor
.logical_position_to_visual(
self.editor.cursor_position.line,
self.editor.cursor_position.column,
)
.map(|(_, x)| x);
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.insert_text,
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_apply_text_edits_command(
&mut self,
mut edits: Vec<TextEditSpec>,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
if edits.is_empty() {
return Ok(CommandResult::Success);
}
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let max_offset = before_char_count;
for edit in &mut edits {
if edit.start > edit.end {
return Err(CommandError::InvalidRange {
start: edit.start,
end: edit.end,
});
}
if edit.end > max_offset {
return Err(CommandError::InvalidRange {
start: edit.start,
end: edit.end,
});
}
edit.text = crate::text::normalize_crlf_to_lf_string(edit.text.clone());
}
edits.sort_by_key(|e| (e.start, e.end));
let mut prev_end = 0usize;
for (idx, edit) in edits.iter().enumerate() {
if idx > 0 && edit.start < prev_end {
return Err(CommandError::Other(
"ApplyTextEdits requires non-overlapping edits".to_string(),
));
}
prev_end = prev_end.max(edit.end);
}
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
inserted_text: String,
inserted_len: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(edits.len());
for edit in edits {
let delete_len = edit.end.saturating_sub(edit.start);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(edit.start, delete_len)
};
let inserted_text = edit.text;
let inserted_len = inserted_text.chars().count();
ops.push(Op {
start_before: edit.start,
start_after: edit.start,
delete_len,
deleted_text,
inserted_text,
inserted_len,
});
}
let mut delta: i64 = 0;
for op in &mut ops {
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"ApplyTextEdits produced an invalid intermediate offset".to_string(),
));
}
op.start_after = effective_start as usize;
delta += op.inserted_len as i64 - op.delete_len as i64;
}
let apply_ops: Vec<(usize, usize, &str)> = ops
.iter()
.map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
.collect();
self.apply_text_ops(apply_ops)?;
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_before,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.inserted_text,
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_apply_snippet_command(
&mut self,
start: usize,
end: usize,
snippet: String,
additional_edits: Vec<TextEditSpec>,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
self.snippet_session = None;
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
if start > end {
return Err(CommandError::InvalidRange { start, end });
}
if end > before_char_count {
return Err(CommandError::InvalidRange { start, end });
}
let template = parse_snippet(&snippet);
let mut edits: Vec<TextEditSpec> = Vec::with_capacity(1 + additional_edits.len());
edits.push(TextEditSpec {
start,
end,
text: template.text.clone(),
});
edits.extend(additional_edits);
let max_offset = before_char_count;
for edit in &mut edits {
if edit.start > edit.end {
return Err(CommandError::InvalidRange {
start: edit.start,
end: edit.end,
});
}
if edit.end > max_offset {
return Err(CommandError::InvalidRange {
start: edit.start,
end: edit.end,
});
}
edit.text = crate::text::normalize_crlf_to_lf_string(edit.text.clone());
}
edits.sort_by_key(|e| (e.start, e.end));
let mut prev_end = 0usize;
for (idx, edit) in edits.iter().enumerate() {
if idx > 0 && edit.start < prev_end {
return Err(CommandError::Other(
"ApplySnippet requires non-overlapping edits".to_string(),
));
}
prev_end = prev_end.max(edit.end);
}
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
inserted_text: String,
inserted_len: usize,
is_main: bool,
}
let mut ops: Vec<Op> = Vec::with_capacity(edits.len());
for edit in edits {
let delete_len = edit.end.saturating_sub(edit.start);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(edit.start, delete_len)
};
let inserted_text = edit.text;
let inserted_len = inserted_text.chars().count();
ops.push(Op {
start_before: edit.start,
start_after: edit.start,
delete_len,
deleted_text,
inserted_text,
inserted_len,
is_main: edit.start == start && edit.end == end,
});
}
let mut delta: i64 = 0;
for op in &mut ops {
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"ApplySnippet produced an invalid intermediate offset".to_string(),
));
}
op.start_after = effective_start as usize;
delta += op.inserted_len as i64 - op.delete_len as i64;
}
let apply_ops: Vec<(usize, usize, &str)> = ops
.iter()
.map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
.collect();
self.apply_text_ops(apply_ops)?;
let main_start_after = ops
.iter()
.find(|op| op.is_main)
.map(|op| op.start_after)
.unwrap_or(start);
if let Some(session) = SnippetSession::new(main_start_after, &template) {
let ranges = session.current_ranges();
if ranges.is_empty() {
self.snippet_session = None;
} else {
let doc_char_count = self.editor.char_count();
let mut selections: Vec<Selection> = Vec::with_capacity(ranges.len());
for (a, b) in ranges {
let start = a.min(doc_char_count);
let end = b.min(doc_char_count);
let (s_line, s_col) = self.editor.line_index.char_offset_to_position(start);
let (e_line, e_col) = self.editor.line_index.char_offset_to_position(end);
selections.push(Selection {
start: Position::new(s_line, s_col),
end: Position::new(e_line, e_col),
direction: SelectionDirection::Forward,
});
}
let (selections, primary_index) =
crate::selection_set::normalize_selections(selections, 0);
self.restore_selection_set(SelectionSetSnapshot {
selections,
primary_index,
});
let pos = self.editor.cursor_position;
self.preferred_x_cells = self
.editor
.logical_position_to_visual(pos.line, pos.column)
.map(|(_, x)| x);
self.snippet_session = Some(session);
}
} else {
let doc_char_count = self.editor.char_count();
let target = main_start_after
.saturating_add(template.final_offset)
.min(doc_char_count);
let (line, column) = self.editor.line_index.char_offset_to_position(target);
let pos = Position::new(line, column);
self.editor.cursor_position = pos;
self.editor.selection = None;
self.editor.secondary_selections.clear();
self.preferred_x_cells = self
.editor
.logical_position_to_visual(pos.line, pos.column)
.map(|(_, x)| x);
}
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_before,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.inserted_text,
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn word_token_range_in_line(
&self,
line_text: &str,
column: usize,
) -> Option<(usize, usize)> {
if line_text.is_empty() {
return None;
}
let chars: Vec<char> = line_text.chars().collect();
if chars.is_empty() {
return None;
}
let len = chars.len();
let col = column.min(len);
let idx = if col < len {
col
} else {
len.saturating_sub(1)
};
let classify = |ch: char| self.editor.word_boundary.is_word_token_char(ch);
let token_span_at = |i: usize| -> (usize, usize) {
let is_token = classify(chars[i]);
if !is_token {
return (i, i + 1);
}
if chars[i].is_ascii() && self.editor.word_boundary.is_ascii_word_char(chars[i]) {
let mut start = i;
while start > 0
&& chars[start - 1].is_ascii()
&& self
.editor
.word_boundary
.is_ascii_word_char(chars[start - 1])
{
start -= 1;
}
let mut end = i + 1;
while end < len
&& chars[end].is_ascii()
&& self.editor.word_boundary.is_ascii_word_char(chars[end])
{
end += 1;
}
(start, end)
} else {
(i, i + 1)
}
};
if classify(chars[idx]) {
return Some(token_span_at(idx));
}
#[allow(clippy::needless_range_loop)]
for i in (idx + 1)..len {
if classify(chars[i]) {
return Some(token_span_at(i));
}
}
let mut i = idx;
while i > 0 {
i -= 1;
if classify(chars[i]) {
return Some(token_span_at(i));
}
}
None
}
pub(super) fn execute_insert_command(
&mut self,
offset: usize,
text: String,
) -> Result<CommandResult, CommandError> {
if text.is_empty() {
return Err(CommandError::EmptyText);
}
let text = crate::text::normalize_crlf_to_lf_string(text);
let max_offset = self.editor.char_count();
if offset > max_offset {
return Err(CommandError::InvalidOffset(offset));
}
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let line_delta = self.apply_text_change_to_line_index_and_layout(offset, "", &text);
if line_delta != 0 {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let inserted_len = text.chars().count();
self.update_interval_trees_for_text_edits(&[IntervalTextEdit::new(
offset,
0,
inserted_len,
)]);
self.normalize_cursor_and_selection();
let after_selection = self.snapshot_selection_set();
let step = UndoStep {
group_id: 0,
edits: vec![TextEdit {
start_before: offset,
start_after: offset,
deleted_text: String::new(),
inserted_text: text.clone(),
}],
before_selection,
after_selection,
};
let coalescible_insert = !text.contains('\n');
let group_id = self.undo_redo.push_step(step, coalescible_insert);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: vec![TextDeltaEdit {
start: offset,
deleted_text: String::new(),
inserted_text: text,
}],
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_delete_command(
&mut self,
start: usize,
length: usize,
) -> Result<CommandResult, CommandError> {
if length == 0 {
return Ok(CommandResult::Success);
}
let before_char_count = self.editor.char_count();
let max_offset = self.editor.char_count();
if start > max_offset {
return Err(CommandError::InvalidOffset(start));
}
if start + length > max_offset {
return Err(CommandError::InvalidRange {
start,
end: start + length,
});
}
let before_selection = self.snapshot_selection_set();
let deleted_text = self.editor.text_range(start, length);
let delta_deleted_text = deleted_text.clone();
let line_delta =
self.apply_text_change_to_line_index_and_layout(start, &delta_deleted_text, "");
if line_delta != 0 {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
self.update_interval_trees_for_text_edits(&[IntervalTextEdit::new(start, length, 0)]);
self.normalize_cursor_and_selection();
let after_selection = self.snapshot_selection_set();
let step = UndoStep {
group_id: 0,
edits: vec![TextEdit {
start_before: start,
start_after: start,
deleted_text,
inserted_text: String::new(),
}],
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: vec![TextDeltaEdit {
start,
deleted_text: delta_deleted_text,
inserted_text: String::new(),
}],
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_replace_command(
&mut self,
start: usize,
length: usize,
text: String,
coalesce_undo: bool,
selection_after: Option<(usize, usize)>,
) -> Result<CommandResult, CommandError> {
let before_char_count = self.editor.char_count();
let max_offset = self.editor.char_count();
if start > max_offset {
return Err(CommandError::InvalidOffset(start));
}
if start + length > max_offset {
return Err(CommandError::InvalidRange {
start,
end: start + length,
});
}
if length == 0 && text.is_empty() {
return Ok(CommandResult::Success);
}
let text = crate::text::normalize_crlf_to_lf_string(text);
let before_selection = self.snapshot_selection_set();
let deleted_text = if length == 0 {
String::new()
} else {
self.editor.text_range(start, length)
};
let delta_deleted_text = deleted_text.clone();
let delta_inserted_text = text.clone();
let inserted_len = text.chars().count();
self.update_interval_trees_for_text_edits(&[IntervalTextEdit::new(
start,
length,
inserted_len,
)]);
let line_delta =
self.apply_text_change_to_line_index_and_layout(start, &deleted_text, &text);
if line_delta != 0 {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
self.normalize_cursor_and_selection();
if let Some((mut sel_start_off, mut sel_end_off)) = selection_after {
let doc_char_count = self.editor.char_count();
sel_start_off = sel_start_off.min(doc_char_count);
sel_end_off = sel_end_off.min(doc_char_count);
let (a_line, a_col) = self
.editor
.line_index
.char_offset_to_position(sel_start_off);
let (b_line, b_col) = self.editor.line_index.char_offset_to_position(sel_end_off);
let start_pos = Position::new(a_line, a_col);
let end_pos = Position::new(b_line, b_col);
self.editor.cursor_position = end_pos;
self.editor.secondary_selections.clear();
if sel_start_off == sel_end_off {
self.editor.selection = None;
} else {
self.editor.selection = Some(Selection {
start: start_pos,
end: end_pos,
direction: crate::selection_set::selection_direction(start_pos, end_pos),
});
}
self.preferred_x_cells = self
.editor
.logical_position_to_visual(end_pos.line, end_pos.column)
.map(|(_, x)| x);
}
let after_selection = self.snapshot_selection_set();
let step = UndoStep {
group_id: 0,
edits: vec![TextEdit {
start_before: start,
start_after: start,
deleted_text,
inserted_text: text,
}],
before_selection,
after_selection,
};
let group_id = if coalesce_undo {
self.undo_redo.push_explicit_coalescing_step(step)
} else {
self.undo_redo.push_step(step, false)
};
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: vec![TextDeltaEdit {
start,
deleted_text: delta_deleted_text,
inserted_text: delta_inserted_text,
}],
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn cursor_char_offset(&self) -> usize {
self.position_to_char_offset_clamped(self.editor.cursor_position)
}
pub(super) fn primary_selection_char_range(&self) -> Option<SearchMatch> {
let selection = self.editor.selection.as_ref()?;
let (min_pos, max_pos) = crate::selection_set::selection_min_max(selection);
let start = self.position_to_char_offset_clamped(min_pos);
let end = self.position_to_char_offset_clamped(max_pos);
if start == end {
None
} else {
Some(SearchMatch { start, end })
}
}
pub(super) fn set_primary_selection_by_char_range(&mut self, range: SearchMatch) {
let (start_line, start_col) = self.editor.line_index.char_offset_to_position(range.start);
let (end_line, end_col) = self.editor.line_index.char_offset_to_position(range.end);
self.editor.cursor_position = Position::new(end_line, end_col);
self.editor.secondary_selections.clear();
if range.start == range.end {
self.editor.selection = None;
} else {
self.editor.selection = Some(Selection {
start: Position::new(start_line, start_col),
end: Position::new(end_line, end_col),
direction: SelectionDirection::Forward,
});
}
}
pub(super) fn execute_find_command(
&mut self,
query: String,
options: SearchOptions,
forward: bool,
) -> Result<CommandResult, CommandError> {
if query.is_empty() {
return Ok(CommandResult::SearchNotFound);
}
let text = self.editor.get_text();
let from = if let Some(selection) = self.primary_selection_char_range() {
if forward {
selection.end
} else {
selection.start
}
} else {
self.cursor_char_offset()
};
let found = if forward {
find_next(&text, &query, options, from)
} else {
find_prev(&text, &query, options, from)
}
.map_err(|err| CommandError::Other(err.to_string()))?;
let Some(m) = found else {
return Ok(CommandResult::SearchNotFound);
};
self.set_primary_selection_by_char_range(m);
Ok(CommandResult::SearchMatch {
start: m.start,
end: m.end,
})
}
pub(super) fn compile_user_regex(
query: &str,
options: SearchOptions,
) -> Result<regex::Regex, CommandError> {
RegexBuilder::new(query)
.case_insensitive(!options.case_sensitive)
.multi_line(true)
.build()
.map_err(|err| CommandError::Other(format!("Invalid regex: {}", err)))
}
pub(super) fn regex_expand_replacement(
re: ®ex::Regex,
text: &str,
index: &CharIndex,
range: SearchMatch,
replacement: &str,
) -> Result<String, CommandError> {
let start_byte = index.char_to_byte(range.start);
let end_byte = index.char_to_byte(range.end);
let caps = re
.captures_at(text, start_byte)
.ok_or_else(|| CommandError::Other("Regex match not found".to_string()))?;
let whole = caps
.get(0)
.ok_or_else(|| CommandError::Other("Regex match missing capture 0".to_string()))?;
if whole.start() != start_byte || whole.end() != end_byte {
return Err(CommandError::Other(
"Regex match did not align with the selected range".to_string(),
));
}
let mut expanded = String::new();
caps.expand(replacement, &mut expanded);
Ok(expanded)
}
pub(super) fn execute_replace_current_command(
&mut self,
query: String,
replacement: String,
options: SearchOptions,
) -> Result<CommandResult, CommandError> {
if query.is_empty() {
return Err(CommandError::Other("Search query is empty".to_string()));
}
let text = self.editor.get_text();
let selection_range = self.primary_selection_char_range();
let mut target = None::<SearchMatch>;
if let Some(range) = selection_range {
let is_match = crate::search::is_match_exact(&text, &query, options, range)
.map_err(|err| CommandError::Other(err.to_string()))?;
if is_match {
target = Some(range);
}
}
if target.is_none() {
let from = self.cursor_char_offset();
target = find_next(&text, &query, options, from)
.map_err(|err| CommandError::Other(err.to_string()))?;
}
let Some(target) = target else {
return Err(CommandError::Other("No match found".to_string()));
};
let index = CharIndex::new(&text);
let inserted_text = if options.regex {
let re = Self::compile_user_regex(&query, options)?;
Self::regex_expand_replacement(&re, &text, &index, target, &replacement)?
} else {
replacement
};
let inserted_text = crate::text::normalize_crlf_to_lf_string(inserted_text);
let deleted_text = self.editor.text_range(target.start, target.len());
let before_char_count = self.editor.char_count();
let delta_deleted_text = deleted_text.clone();
let before_selection = self.snapshot_selection_set();
self.apply_text_ops(vec![(target.start, target.len(), inserted_text.as_str())])?;
let inserted_len = inserted_text.chars().count();
let new_range = SearchMatch {
start: target.start,
end: target.start + inserted_len,
};
self.set_primary_selection_by_char_range(new_range);
let after_selection = self.snapshot_selection_set();
let step = UndoStep {
group_id: 0,
edits: vec![TextEdit {
start_before: target.start,
start_after: target.start,
deleted_text,
inserted_text: inserted_text.clone(),
}],
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: vec![TextDeltaEdit {
start: target.start,
deleted_text: delta_deleted_text,
inserted_text,
}],
undo_group_id: Some(group_id),
});
Ok(CommandResult::ReplaceResult { replaced: 1 })
}
pub(super) fn execute_replace_all_command(
&mut self,
query: String,
replacement: String,
options: SearchOptions,
) -> Result<CommandResult, CommandError> {
if query.is_empty() {
return Err(CommandError::Other("Search query is empty".to_string()));
}
let replacement = crate::text::normalize_crlf_to_lf_string(replacement);
let text = self.editor.get_text();
let matches =
find_all(&text, &query, options).map_err(|err| CommandError::Other(err.to_string()))?;
if matches.is_empty() {
return Err(CommandError::Other("No match found".to_string()));
}
let match_count = matches.len();
let index = CharIndex::new(&text);
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
inserted_text: String,
inserted_len: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(match_count);
if options.regex {
let re = Self::compile_user_regex(&query, options)?;
for m in matches {
let deleted_text = {
let start_byte = index.char_to_byte(m.start);
let end_byte = index.char_to_byte(m.end);
text.get(start_byte..end_byte)
.unwrap_or_default()
.to_string()
};
let inserted_text =
Self::regex_expand_replacement(&re, &text, &index, m, &replacement)?;
let inserted_text = crate::text::normalize_crlf_to_lf_string(inserted_text);
let inserted_len = inserted_text.chars().count();
ops.push(Op {
start_before: m.start,
start_after: m.start,
delete_len: m.len(),
deleted_text,
inserted_text,
inserted_len,
});
}
} else {
let inserted_len = replacement.chars().count();
for m in matches {
let deleted_text = {
let start_byte = index.char_to_byte(m.start);
let end_byte = index.char_to_byte(m.end);
text.get(start_byte..end_byte)
.unwrap_or_default()
.to_string()
};
ops.push(Op {
start_before: m.start,
start_after: m.start,
delete_len: m.len(),
deleted_text,
inserted_text: replacement.clone(),
inserted_len,
});
}
}
ops.sort_by_key(|op| op.start_before);
let mut delta: i64 = 0;
for op in &mut ops {
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"ReplaceAll produced an invalid intermediate offset".to_string(),
));
}
op.start_after = effective_start as usize;
delta += op.inserted_len as i64 - op.delete_len as i64;
}
let before_char_count = self.editor.char_count();
let before_selection = self.snapshot_selection_set();
let apply_ops: Vec<(usize, usize, &str)> = ops
.iter()
.map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
.collect();
self.apply_text_ops(apply_ops)?;
if let Some(first) = ops.first() {
let caret_end = first.start_after + first.inserted_len;
let select_end = if first.inserted_len == 0 {
first.start_after
} else {
caret_end
};
self.set_primary_selection_by_char_range(SearchMatch {
start: first.start_after,
end: select_end,
});
} else {
self.editor.selection = None;
self.editor.secondary_selections.clear();
}
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_before,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: op.inserted_text,
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::ReplaceResult {
replaced: match_count,
})
}
pub(super) fn execute_backspace_command(&mut self) -> Result<CommandResult, CommandError> {
self.execute_delete_like_command(false)
}
pub(super) fn execute_delete_forward_command(&mut self) -> Result<CommandResult, CommandError> {
self.execute_delete_like_command(true)
}
pub(super) fn execute_delete_to_prev_tab_stop_command(
&mut self,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_selection = self.snapshot_selection_set();
let selections = before_selection.selections.clone();
let primary_index = before_selection.primary_index;
let tab_width = self.editor.layout_engine.tab_width().max(1);
#[derive(Debug)]
struct Op {
selection_index: usize,
start_offset: usize,
delete_len: usize,
deleted_text: String,
start_after: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
(selection.start, selection.end)
} else {
(selection.end, selection.start)
};
let (start_offset, end_offset) = if range_start_pos != range_end_pos {
let start_offset = self.position_to_char_offset_clamped(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
if start_offset <= end_offset {
(start_offset, end_offset)
} else {
(end_offset, start_offset)
}
} else {
let caret = selection.end;
let caret_offset = self.position_to_char_offset_clamped(caret);
if caret_offset == 0 {
(0, 0)
} else {
let line_text = self
.editor
.line_index
.get_line_text(caret.line)
.unwrap_or_default();
let line_char_len = line_text.chars().count();
let col = caret.column.min(line_char_len);
let in_leading_whitespace = line_text
.chars()
.take(col)
.all(|ch| ch == ' ' || ch == '\t');
if !in_leading_whitespace {
(caret_offset - 1, caret_offset)
} else {
let x_in_line = visual_x_for_column(&line_text, col, tab_width);
let back = if x_in_line == 0 {
0
} else {
let rem = x_in_line % tab_width;
if rem == 0 { tab_width } else { rem }
};
let target_x = x_in_line.saturating_sub(back);
let mut target_col = col;
while target_col > 0 {
let prev_col = target_col - 1;
let prev_x = visual_x_for_column(&line_text, prev_col, tab_width);
if prev_x < target_x {
break;
}
target_col = prev_col;
if prev_x == target_x {
break;
}
}
let target_offset = self
.editor
.line_index
.position_to_char_offset(caret.line, target_col);
(target_offset, caret_offset)
}
}
};
let delete_len = end_offset.saturating_sub(start_offset);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
ops.push(Op {
selection_index,
start_offset,
delete_len,
deleted_text,
start_after: start_offset,
});
}
if !ops.iter().any(|op| op.delete_len > 0) {
return Ok(CommandResult::Success);
}
let before_char_count = self.editor.char_count();
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
caret_offsets[op.selection_index] = effective_start;
delta -= op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, 0)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
if op.delete_len == 0 {
continue;
}
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
"",
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
for offset in &caret_offsets {
let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == new_primary_index {
None
} else {
Some(sel)
}
})
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: String::new(),
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_delete_by_boundary_command(
&mut self,
forward: bool,
boundary: TextBoundary,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_selection = self.snapshot_selection_set();
let selections = before_selection.selections.clone();
let primary_index = before_selection.primary_index;
let doc_char_count = self.editor.char_count();
#[derive(Debug)]
struct Op {
selection_index: usize,
start_offset: usize,
delete_len: usize,
deleted_text: String,
start_after: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
(selection.start, selection.end)
} else {
(selection.end, selection.start)
};
let (start_offset, end_offset) = if range_start_pos != range_end_pos {
let start_offset = self.position_to_char_offset_clamped(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
if start_offset <= end_offset {
(start_offset, end_offset)
} else {
(end_offset, start_offset)
}
} else {
let caret = selection.end;
let caret_offset = self.position_to_char_offset_clamped(caret);
let line_count = self.editor.line_index.line_count();
let line = caret.line.min(line_count.saturating_sub(1));
let line_text = self
.editor
.line_index
.get_line_text(line)
.unwrap_or_default();
let line_char_len = line_text.chars().count();
let col = caret.column.min(line_char_len);
if forward {
if caret_offset >= doc_char_count {
(caret_offset, caret_offset)
} else if col >= line_char_len {
(caret_offset, (caret_offset + 1).min(doc_char_count))
} else {
let next_col = next_boundary_column(&line_text, col, boundary);
let start_offset =
self.editor.line_index.position_to_char_offset(line, col);
let end_offset = self
.editor
.line_index
.position_to_char_offset(line, next_col);
(start_offset, end_offset)
}
} else if caret_offset == 0 {
(0, 0)
} else if col == 0 {
(caret_offset - 1, caret_offset)
} else {
let prev_col = prev_boundary_column(&line_text, col, boundary);
let start_offset = self
.editor
.line_index
.position_to_char_offset(line, prev_col);
let end_offset = self.editor.line_index.position_to_char_offset(line, col);
(start_offset, end_offset)
}
};
let delete_len = end_offset.saturating_sub(start_offset);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
ops.push(Op {
selection_index,
start_offset,
delete_len,
deleted_text,
start_after: start_offset,
});
}
if !ops.iter().any(|op| op.delete_len > 0) {
return Ok(CommandResult::Success);
}
let before_char_count = self.editor.char_count();
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
caret_offsets[op.selection_index] = effective_start;
delta -= op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, 0)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
if op.delete_len == 0 {
continue;
}
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
"",
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
for offset in &caret_offsets {
let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == new_primary_index {
None
} else {
Some(sel)
}
})
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: String::new(),
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: e.inserted_text.clone(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn execute_delete_like_command(
&mut self,
forward: bool,
) -> Result<CommandResult, CommandError> {
self.undo_redo.end_group();
let before_selection = self.snapshot_selection_set();
let selections = before_selection.selections.clone();
let primary_index = before_selection.primary_index;
let doc_char_count = self.editor.char_count();
#[derive(Debug)]
struct Op {
selection_index: usize,
start_offset: usize,
delete_len: usize,
deleted_text: String,
start_after: usize,
}
let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
for (selection_index, selection) in selections.iter().enumerate() {
let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
(selection.start, selection.end)
} else {
(selection.end, selection.start)
};
let (start_offset, end_offset) = if range_start_pos != range_end_pos {
let start_offset = self.position_to_char_offset_clamped(range_start_pos);
let end_offset = self.position_to_char_offset_clamped(range_end_pos);
if start_offset <= end_offset {
(start_offset, end_offset)
} else {
(end_offset, start_offset)
}
} else {
let caret_offset = self.position_to_char_offset_clamped(selection.end);
let maybe_pair_delete = (self.auto_pairs.enabled && self.auto_pairs.delete_pair)
.then(|| {
if caret_offset == 0 || caret_offset >= doc_char_count {
return None;
}
let open = self.editor.line_index.char_at(caret_offset - 1)?;
let close = self.editor.line_index.char_at(caret_offset)?;
self.auto_pairs
.is_matching_pair(open, close)
.then_some((caret_offset - 1, (caret_offset + 1).min(doc_char_count)))
})
.flatten();
if let Some(range) = maybe_pair_delete {
range
} else if forward {
if caret_offset >= doc_char_count {
(caret_offset, caret_offset)
} else {
(caret_offset, (caret_offset + 1).min(doc_char_count))
}
} else if caret_offset == 0 {
(0, 0)
} else {
(caret_offset - 1, caret_offset)
}
};
let delete_len = end_offset.saturating_sub(start_offset);
let deleted_text = if delete_len == 0 {
String::new()
} else {
self.editor.text_range(start_offset, delete_len)
};
ops.push(Op {
selection_index,
start_offset,
delete_len,
deleted_text,
start_after: start_offset,
});
}
if !ops.iter().any(|op| op.delete_len > 0) {
return Ok(CommandResult::Success);
}
let before_char_count = self.editor.char_count();
let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = (op.start_offset as i64 + delta) as usize;
op.start_after = effective_start;
caret_offsets[op.selection_index] = effective_start;
delta -= op.delete_len as i64;
}
let mut desc_indices = asc_indices;
desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
let interval_edits: Vec<IntervalTextEdit> = desc_indices
.iter()
.map(|&idx| {
let op = &ops[idx];
IntervalTextEdit::new(op.start_offset, op.delete_len, 0)
})
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for &idx in &desc_indices {
let op = &ops[idx];
if op.delete_len == 0 {
continue;
}
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start_offset,
&op.deleted_text,
"",
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
for offset in &caret_offsets {
let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, new_primary_index) =
crate::selection_set::normalize_selections(new_carets, primary_index);
let primary = new_carets
.get(new_primary_index)
.cloned()
.ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
self.editor.cursor_position = primary.end;
self.editor.selection = None;
self.editor.secondary_selections = new_carets
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == new_primary_index {
None
} else {
Some(sel)
}
})
.collect();
let after_selection = self.snapshot_selection_set();
let edits: Vec<TextEdit> = ops
.into_iter()
.map(|op| TextEdit {
start_before: op.start_offset,
start_after: op.start_after,
deleted_text: op.deleted_text,
inserted_text: String::new(),
})
.collect();
let mut delta_edits: Vec<TextDeltaEdit> = edits
.iter()
.map(|e| TextDeltaEdit {
start: e.start_before,
deleted_text: e.deleted_text.clone(),
inserted_text: String::new(),
})
.collect();
delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
let step = UndoStep {
group_id: 0,
edits,
before_selection,
after_selection,
};
let group_id = self.undo_redo.push_step(step, false);
self.last_text_delta = Some(TextDelta {
before_char_count,
after_char_count: self.editor.char_count(),
edits: delta_edits,
undo_group_id: Some(group_id),
});
Ok(CommandResult::Success)
}
pub(super) fn snapshot_selection_set(&self) -> SelectionSetSnapshot {
let mut selections: Vec<Selection> =
Vec::with_capacity(1 + self.editor.secondary_selections.len());
let primary = self.editor.selection.clone().unwrap_or(Selection {
start: self.editor.cursor_position,
end: self.editor.cursor_position,
direction: SelectionDirection::Forward,
});
selections.push(primary);
selections.extend(self.editor.secondary_selections.iter().cloned());
let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
SelectionSetSnapshot {
selections,
primary_index,
}
}
pub(super) fn restore_selection_set(&mut self, snapshot: SelectionSetSnapshot) {
if snapshot.selections.is_empty() {
self.editor.cursor_position = Position::new(0, 0);
self.editor.selection = None;
self.editor.secondary_selections.clear();
return;
}
let primary = snapshot
.selections
.get(snapshot.primary_index)
.cloned()
.unwrap_or_else(|| snapshot.selections[0].clone());
self.editor.cursor_position = primary.end;
self.editor.selection = if primary.start == primary.end {
None
} else {
Some(primary.clone())
};
self.editor.secondary_selections = snapshot
.selections
.into_iter()
.enumerate()
.filter_map(|(idx, sel)| {
if idx == snapshot.primary_index {
None
} else {
Some(sel)
}
})
.collect();
self.normalize_cursor_and_selection();
}
pub(super) fn apply_undo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
for edit in edits {
let start = edit.start_after;
let delete_len = edit.inserted_len();
let insert_text = edit.deleted_text.as_str();
ops.push((start, delete_len, insert_text));
}
self.apply_text_ops(ops)
}
pub(super) fn apply_redo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
for edit in edits {
let start = edit.start_before;
let delete_len = edit.deleted_len();
let insert_text = edit.inserted_text.as_str();
ops.push((start, delete_len, insert_text));
}
self.apply_text_ops(ops)
}
pub(super) fn apply_text_ops(
&mut self,
mut ops: Vec<(usize, usize, &str)>,
) -> Result<(), CommandError> {
ops.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
struct PreparedTextOp<'a> {
start: usize,
delete_len: usize,
deleted_text: String,
insert_text: &'a str,
insert_len: usize,
}
let max_offset = self.editor.char_count();
let mut prepared_ops = Vec::with_capacity(ops.len());
for (start, delete_len, insert_text) in ops {
let end = start
.checked_add(delete_len)
.ok_or(CommandError::InvalidRange {
start,
end: usize::MAX,
})?;
if start > max_offset {
return Err(CommandError::InvalidOffset(start));
}
if end > max_offset {
return Err(CommandError::InvalidRange { start, end });
}
let deleted_text = if delete_len > 0 {
self.editor.text_range(start, delete_len)
} else {
String::new()
};
let insert_len = insert_text.chars().count();
prepared_ops.push(PreparedTextOp {
start,
delete_len,
deleted_text,
insert_text,
insert_len,
});
}
let interval_edits: Vec<IntervalTextEdit> = prepared_ops
.iter()
.map(|op| IntervalTextEdit::new(op.start, op.delete_len, op.insert_len))
.collect();
self.update_interval_trees_for_text_edits(&interval_edits);
let mut folding_line_changed = false;
for op in prepared_ops {
folding_line_changed |= self.apply_text_change_to_line_index_and_layout(
op.start,
&op.deleted_text,
op.insert_text,
) != 0;
}
if folding_line_changed {
self.editor
.folding_manager
.clamp_to_line_count(self.editor.line_index.line_count());
}
self.normalize_cursor_and_selection();
Ok(())
}
pub(super) fn apply_text_change_to_line_index_and_layout(
&mut self,
start_offset: usize,
deleted_text: &str,
inserted_text: &str,
) -> isize {
let start_line = self
.editor
.line_index
.char_offset_to_position(start_offset)
.0;
let deleted_newlines = deleted_text
.as_bytes()
.iter()
.filter(|b| **b == b'\n')
.count();
let inserted_newlines = inserted_text
.as_bytes()
.iter()
.filter(|b| **b == b'\n')
.count();
let line_delta = inserted_newlines as isize - deleted_newlines as isize;
if line_delta != 0 {
self.editor
.folding_manager
.apply_line_delta(start_line, line_delta);
}
let deleted_chars = deleted_text.chars().count();
if deleted_chars > 0 {
self.editor.line_index.delete(start_offset, deleted_chars);
}
if !inserted_text.is_empty() {
self.editor.line_index.insert(start_offset, inserted_text);
}
#[cfg(debug_assertions)]
{
if deleted_chars > 0 {
self.editor
.piece_table_shadow
.delete(start_offset, deleted_chars);
}
if !inserted_text.is_empty() {
self.editor
.piece_table_shadow
.insert(start_offset, inserted_text);
}
}
self.assert_piece_table_text_buffer_consistent();
if line_delta > 0 {
for i in 0..(line_delta as usize) {
let line = start_line.saturating_add(1).saturating_add(i);
let line_text = self
.editor
.line_index
.get_line_text(line)
.unwrap_or_default();
self.editor.layout_engine.insert_line(line, &line_text);
}
} else if line_delta < 0 {
for _ in 0..((-line_delta) as usize) {
self.editor
.layout_engine
.delete_line(start_line.saturating_add(1));
}
}
let touch_lines = deleted_newlines.max(inserted_newlines).saturating_add(1);
let end_line = start_line.saturating_add(touch_lines);
let line_count = self.editor.line_index.line_count();
if line_count == 0 {
return line_delta;
}
let last_line = line_count.saturating_sub(1);
for line in start_line..=end_line.min(last_line) {
let line_text = self
.editor
.line_index
.get_line_text(line)
.unwrap_or_default();
self.editor.layout_engine.update_line(line, &line_text);
}
self.editor.sync_visual_row_index_after_text_change(
start_line,
deleted_newlines,
inserted_newlines,
);
line_delta
}
#[cfg(debug_assertions)]
pub(super) fn assert_piece_table_text_buffer_consistent(&self) {
debug_assert_eq!(
self.editor.piece_table_shadow.get_text(),
self.editor.line_index.text_buffer().get_text(),
"PieceTable shadow text diverged from TextBuffer"
);
}
#[cfg(not(debug_assertions))]
pub(super) fn assert_piece_table_text_buffer_consistent(&self) {}
}