use super::*;
impl CommandExecutor {
pub(super) fn selection_char_range(&self, selection: &Selection) -> SearchMatch {
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);
SearchMatch {
start: start.min(end),
end: start.max(end),
}
}
pub(super) fn selected_line_blocks(selections: &[Selection]) -> Vec<(usize, usize)> {
let mut lines: Vec<usize> = Vec::new();
for sel in selections {
let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
for line in min_pos.line..=max_pos.line {
lines.push(line);
}
}
lines.sort_unstable();
lines.dedup();
let mut blocks: Vec<(usize, usize)> = Vec::new();
for line in lines {
if let Some((_, end)) = blocks.last_mut()
&& *end + 1 == line
{
*end = line;
continue;
}
blocks.push((line, line));
}
blocks
}
pub(super) fn slice_text_for_lines(&self, start_line: usize, end_line: usize) -> String {
let line_count = self.editor.line_index.line_count();
if line_count == 0 || start_line >= line_count || start_line > end_line {
return String::new();
}
let end_line = end_line.min(line_count - 1);
let start_offset = self
.editor
.line_index
.position_to_char_offset(start_line, 0);
let end_offset = if end_line + 1 < line_count {
self.editor
.line_index
.position_to_char_offset(end_line + 1, 0)
} else {
self.editor.char_count()
};
if end_offset <= start_offset {
return String::new();
}
self.editor
.text_range(start_offset, end_offset - start_offset)
}
pub(super) fn execute_duplicate_lines_command(
&mut self,
) -> 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 primary_index = before_selection.primary_index;
let line_count = self.editor.line_index.line_count();
if line_count == 0 {
return Ok(CommandResult::Success);
}
let blocks = Self::selected_line_blocks(&selections);
if blocks.is_empty() {
return Ok(CommandResult::Success);
}
let doc_ends_with_newline = before_char_count > 0
&& self
.editor
.line_index
.char_at(before_char_count - 1)
.is_some_and(|ch| ch == '\n');
struct Op {
start_before: usize,
start_after: usize,
deleted_text: String,
inserted_text: String,
inserted_len: usize,
}
let mut ops: Vec<Op> = Vec::new();
for (start_line, end_line) in blocks {
if start_line >= line_count {
continue;
}
let end_line = end_line.min(line_count - 1);
let insertion_offset = if end_line + 1 < line_count {
self.editor
.line_index
.position_to_char_offset(end_line + 1, 0)
} else {
before_char_count
};
let block_text = self.slice_text_for_lines(start_line, end_line);
if block_text.is_empty() && before_char_count == 0 {
continue;
}
let mut inserted_text = block_text;
if insertion_offset == before_char_count
&& !doc_ends_with_newline
&& before_char_count > 0
{
inserted_text.insert(0, '\n');
}
let inserted_len = inserted_text.chars().count();
if inserted_len == 0 {
continue;
}
ops.push(Op {
start_before: insertion_offset,
start_after: insertion_offset,
deleted_text: String::new(),
inserted_text,
inserted_len,
});
}
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_before);
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"DuplicateLines produced an invalid intermediate offset".to_string(),
));
}
op.start_after = effective_start as usize;
delta += op.inserted_len as i64;
}
let apply_ops: Vec<(usize, usize, &str)> = ops
.iter()
.map(|op| (op.start_before, 0usize, op.inserted_text.as_str()))
.collect();
self.apply_text_ops(apply_ops)?;
let mut mapped: Vec<Selection> = Vec::with_capacity(selections.len());
let mut block_info: Vec<(usize, usize, usize, usize)> = Vec::new(); let mut cumulative = 0usize;
let mut blocks = Self::selected_line_blocks(&selections);
blocks.sort_by_key(|(s, _)| *s);
for (s, e) in blocks {
let size = e.saturating_sub(s) + 1;
block_info.push((s, e, size, cumulative));
cumulative = cumulative.saturating_add(size);
}
let line_index = &self.editor.line_index;
for sel in selections {
let mut start = sel.start;
let mut end = sel.end;
let map_line = |line: usize, info: &[(usize, usize, usize, usize)]| -> usize {
for (s, e, size, shift_before) in info {
if line >= *s && line <= *e {
return line + *shift_before + *size;
}
if line < *s {
break;
}
}
let mut shift = 0usize;
for (s, e, size, shift_before) in info {
let _ = shift_before;
if *e < line {
shift = shift.saturating_add(*size);
} else if line < *s {
break;
}
}
line + shift
};
start.line = map_line(start.line, &block_info);
end.line = map_line(end.line, &block_info);
start.column =
Self::clamp_column_for_line_with_index(line_index, start.line, start.column);
end.column = Self::clamp_column_for_line_with_index(line_index, end.line, end.column);
mapped.push(Selection {
start,
end,
direction: crate::selection_set::selection_direction(start, end),
});
}
let (mapped, mapped_primary) =
crate::selection_set::normalize_selections(mapped, primary_index);
self.execute_cursor(CursorCommand::SetSelections {
selections: mapped,
primary_index: mapped_primary,
})?;
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_delete_lines_command(&mut self) -> 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 primary_selection = selections
.get(before_selection.primary_index)
.cloned()
.unwrap_or_else(|| selections[0].clone());
let line_count = self.editor.line_index.line_count();
if line_count == 0 {
return Ok(CommandResult::Success);
}
let blocks = Self::selected_line_blocks(&selections);
if blocks.is_empty() {
return Ok(CommandResult::Success);
}
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
}
let mut ops: Vec<Op> = Vec::new();
let mut primary_op_index = 0usize;
for (idx, (start_line, end_line)) in blocks.into_iter().enumerate() {
if start_line >= line_count {
continue;
}
let end_line = end_line.min(line_count - 1);
let mut start_offset = self
.editor
.line_index
.position_to_char_offset(start_line, 0);
let end_offset = if end_line + 1 < line_count {
self.editor
.line_index
.position_to_char_offset(end_line + 1, 0)
} else {
before_char_count
};
if end_line + 1 >= line_count && start_offset > 0 {
start_offset = start_offset.saturating_sub(1);
}
if end_offset <= start_offset {
continue;
}
let delete_len = end_offset - start_offset;
let deleted_text = self.editor.text_range(start_offset, delete_len);
if crate::selection_set::selection_contains_position_inclusive(
&primary_selection,
Position::new(start_line, 0),
) {
primary_op_index = idx;
}
ops.push(Op {
start_before: start_offset,
start_after: start_offset,
delete_len,
deleted_text,
});
}
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_before);
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"DeleteLines produced an invalid intermediate offset".to_string(),
));
}
op.start_after = effective_start as usize;
delta -= op.delete_len as i64;
}
let apply_ops: Vec<(usize, usize, &str)> = ops
.iter()
.map(|op| (op.start_before, op.delete_len, ""))
.collect();
self.apply_text_ops(apply_ops)?;
let mut new_carets: Vec<Selection> = Vec::with_capacity(ops.len());
for op in &ops {
let (line, column) = self
.editor
.line_index
.char_offset_to_position(op.start_after);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let primary_index = primary_op_index.min(new_carets.len().saturating_sub(1));
self.execute_cursor(CursorCommand::SetSelections {
selections: new_carets,
primary_index,
})?;
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: 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 execute_move_lines_command(
&mut self,
up: 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 primary_index = before_selection.primary_index;
let line_count = self.editor.line_index.line_count();
if line_count <= 1 {
return Ok(CommandResult::Success);
}
let blocks = Self::selected_line_blocks(&selections);
if blocks.is_empty() {
return Ok(CommandResult::Success);
}
#[derive(Debug, Clone, Copy)]
struct Block {
start: usize,
end: usize,
}
let mut moved_blocks: Vec<Block> = Vec::new();
for (start, end) in blocks {
let start = start.min(line_count - 1);
let end = end.min(line_count - 1);
if up {
if start == 0 {
continue;
}
} else if end + 1 >= line_count {
continue;
}
moved_blocks.push(Block { start, end });
}
if moved_blocks.is_empty() {
return Ok(CommandResult::Success);
}
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
inserted_text: String,
}
let mut ops: Vec<Op> = Vec::with_capacity(moved_blocks.len());
for block in &moved_blocks {
let (range_start_line, range_end_line) = if up {
(block.start - 1, block.end)
} else {
(block.start, block.end + 1)
};
let start_offset = self
.editor
.line_index
.position_to_char_offset(range_start_line, 0);
let end_offset = if range_end_line + 1 < line_count {
self.editor
.line_index
.position_to_char_offset(range_end_line + 1, 0)
} else {
before_char_count
};
if end_offset <= start_offset {
continue;
}
let deleted_text = self
.editor
.text_range(start_offset, end_offset - start_offset);
let block_text = self.slice_text_for_lines(block.start, block.end);
let inserted_text = if up {
let above_text = self.slice_text_for_lines(block.start - 1, block.start - 1);
format!("{}{}", block_text, above_text)
} else {
let below_text = self.slice_text_for_lines(block.end + 1, block.end + 1);
format!("{}{}", below_text, block_text)
};
ops.push(Op {
start_before: start_offset,
start_after: start_offset,
delete_len: end_offset - start_offset,
deleted_text,
inserted_text,
});
}
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_before);
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"MoveLines produced an invalid intermediate offset".to_string(),
));
}
op.start_after = effective_start as usize;
let inserted_len = op.inserted_text.chars().count() as i64;
delta += inserted_len - 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 line_index = &self.editor.line_index;
let mut mapped: Vec<Selection> = Vec::with_capacity(selections.len());
for sel in selections {
let mut start = sel.start;
let mut end = sel.end;
let map_line = |line: usize, moved_blocks: &[Block], up: bool| -> usize {
for block in moved_blocks {
let size = block.end.saturating_sub(block.start) + 1;
if line >= block.start && line <= block.end {
return if up { line - 1 } else { line + 1 };
}
if up && line == block.start - 1 {
return line + size;
}
if !up && line == block.end + 1 {
return line.saturating_sub(size);
}
}
line
};
start.line = map_line(start.line, &moved_blocks, up);
end.line = map_line(end.line, &moved_blocks, up);
start.column =
Self::clamp_column_for_line_with_index(line_index, start.line, start.column);
end.column = Self::clamp_column_for_line_with_index(line_index, end.line, end.column);
mapped.push(Selection {
start,
end,
direction: crate::selection_set::selection_direction(start, end),
});
}
let (mapped, mapped_primary) =
crate::selection_set::normalize_selections(mapped, primary_index);
self.execute_cursor(CursorCommand::SetSelections {
selections: mapped,
primary_index: mapped_primary,
})?;
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_join_lines_command(&mut self) -> 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 line_count = self.editor.line_index.line_count();
if line_count <= 1 {
return Ok(CommandResult::Success);
}
let mut join_lines: Vec<usize> = Vec::new();
for sel in &selections {
let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
if min_pos.line >= line_count {
continue;
}
let last = max_pos.line.min(line_count - 1);
if min_pos.line == last {
join_lines.push(last);
} else {
for line in min_pos.line..last {
join_lines.push(line);
}
}
}
join_lines.sort_unstable();
join_lines.dedup();
join_lines.retain(|l| *l + 1 < line_count);
if join_lines.is_empty() {
return Ok(CommandResult::Success);
}
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(join_lines.len());
join_lines.sort_by_key(|l| std::cmp::Reverse(*l));
for line in join_lines {
let line_text = self
.editor
.line_index
.get_line_text(line)
.unwrap_or_default();
let next_text = self
.editor
.line_index
.get_line_text(line + 1)
.unwrap_or_default();
let line_len = line_text.chars().count();
let join_offset = self
.editor
.line_index
.position_to_char_offset(line, line_len);
let leading_ws = next_text
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.count();
let end_offset = self
.editor
.line_index
.position_to_char_offset(line + 1, leading_ws);
if end_offset <= join_offset {
continue;
}
let left_ends_with_ws = line_text
.chars()
.last()
.is_some_and(|c| c == ' ' || c == '\t');
let right_trimmed_empty = next_text.chars().nth(leading_ws).is_none();
let insert_space = !left_ends_with_ws && !line_text.is_empty() && !right_trimmed_empty;
let inserted_text = if insert_space {
" ".to_string()
} else {
String::new()
};
let inserted_len = inserted_text.chars().count();
let delete_len = end_offset - join_offset;
let deleted_text = self.editor.text_range(join_offset, delete_len);
ops.push(Op {
start_before: join_offset,
start_after: join_offset,
delete_len,
deleted_text,
inserted_text,
inserted_len,
});
}
if ops.is_empty() {
return Ok(CommandResult::Success);
}
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(
"JoinLines 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 mut new_carets: Vec<Selection> = Vec::with_capacity(ops.len());
for op in &ops {
let caret_offset = op.start_after + op.inserted_len;
let (line, column) = self.editor.line_index.char_offset_to_position(caret_offset);
let pos = Position::new(line, column);
new_carets.push(Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
});
}
let (new_carets, primary_index) = crate::selection_set::normalize_selections(new_carets, 0);
self.execute_cursor(CursorCommand::SetSelections {
selections: new_carets,
primary_index,
})?;
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_toggle_comment_command(
&mut self,
config: CommentConfig,
) -> Result<CommandResult, CommandError> {
if !config.has_line() && !config.has_block() {
return Err(CommandError::Other(
"ToggleComment requires at least one comment token".to_string(),
));
}
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 primary_index = before_selection.primary_index;
let line_count = self.editor.line_index.line_count();
if line_count == 0 {
return Ok(CommandResult::Success);
}
let all_single_line_selections = selections.iter().all(|sel| {
let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
min_pos.line == max_pos.line && min_pos != max_pos
});
if config.has_block()
&& all_single_line_selections
&& let (Some(block_start), Some(block_end)) =
(config.block_start.as_deref(), config.block_end.as_deref())
{
return self.execute_toggle_block_comment_inline(
block_start,
block_end,
before_char_count,
before_selection,
selections,
primary_index,
);
}
if config.has_line()
&& let Some(token) = config.line.as_deref()
{
return self.execute_toggle_line_comment(
token,
before_char_count,
before_selection,
selections,
primary_index,
);
}
if config.has_block()
&& let (Some(block_start), Some(block_end)) =
(config.block_start.as_deref(), config.block_end.as_deref())
{
return self.execute_toggle_block_comment_lines(
block_start,
block_end,
before_char_count,
before_selection,
selections,
primary_index,
);
}
Ok(CommandResult::Success)
}
pub(super) fn execute_toggle_line_comment(
&mut self,
token: &str,
before_char_count: usize,
before_selection: SelectionSetSnapshot,
selections: Vec<Selection>,
_primary_index: usize,
) -> Result<CommandResult, CommandError> {
let token = token.trim_end();
if token.is_empty() {
return Ok(CommandResult::Success);
}
let token_len = token.chars().count();
let insert_text = format!("{} ", token);
let insert_len = insert_text.chars().count();
let mut lines: Vec<usize> = Vec::new();
for sel in &selections {
let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
for line in min_pos.line..=max_pos.line {
lines.push(line);
}
}
lines.sort_unstable();
lines.dedup();
lines.retain(|l| *l < self.editor.line_index.line_count());
if lines.is_empty() {
return Ok(CommandResult::Success);
}
struct LineCommentTarget {
line: usize,
text: String,
indent_col: usize,
indent_byte: usize,
}
let mut targets: Vec<LineCommentTarget> = Vec::with_capacity(lines.len());
let mut non_empty = 0usize;
let mut all_commented = true;
for line in lines {
let line_text = self
.editor
.line_index
.get_line_text(line)
.unwrap_or_default();
let (indent_col, indent_byte) = leading_horizontal_whitespace(&line_text);
let rest = line_text.get(indent_byte..).unwrap_or("");
if rest.is_empty() {
targets.push(LineCommentTarget {
line,
text: line_text,
indent_col,
indent_byte,
});
continue;
}
non_empty += 1;
if !rest.starts_with(token) {
all_commented = false;
}
targets.push(LineCommentTarget {
line,
text: line_text,
indent_col,
indent_byte,
});
}
let should_uncomment = non_empty > 0 && all_commented;
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
inserted_text: String,
inserted_len: usize,
line: usize,
indent_col: usize,
col_delta: isize,
}
let mut ops: Vec<Op> = Vec::new();
for target in targets {
let rest = target.text.get(target.indent_byte..).unwrap_or("");
let start_offset = self
.editor
.line_index
.position_to_char_offset(target.line, target.indent_col);
if should_uncomment {
if rest.is_empty() || !rest.starts_with(token) {
continue;
}
let mut remove_len = token_len;
if let Some(ch) = rest
.get(token.len()..)
.and_then(|suffix| suffix.chars().next())
&& ch == ' '
{
remove_len += 1;
}
if remove_len == 0 {
continue;
}
let deleted_text = self.editor.text_range(start_offset, remove_len);
ops.push(Op {
start_before: start_offset,
start_after: start_offset,
delete_len: remove_len,
deleted_text,
inserted_text: String::new(),
inserted_len: 0,
line: target.line,
indent_col: target.indent_col,
col_delta: -(remove_len as isize),
});
} else {
ops.push(Op {
start_before: start_offset,
start_after: start_offset,
delete_len: 0,
deleted_text: String::new(),
inserted_text: insert_text.clone(),
inserted_len: insert_len,
line: target.line,
indent_col: target.indent_col,
col_delta: insert_len 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_before);
let mut delta: i64 = 0;
for &idx in &asc_indices {
let op = &mut ops[idx];
let effective_start = op.start_before as i64 + delta;
if effective_start < 0 {
return Err(CommandError::Other(
"ToggleComment 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)?;
use std::collections::HashMap;
let mut line_deltas: HashMap<usize, (usize, isize)> = HashMap::new();
for op in &ops {
line_deltas.insert(op.line, (op.indent_col, op.col_delta));
}
let line_index = &self.editor.line_index;
let apply_delta = |pos: &mut Position, deltas: &HashMap<usize, (usize, isize)>| {
let Some((indent_col, delta)) = deltas.get(&pos.line) else {
return;
};
if pos.column < *indent_col {
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();
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_toggle_block_comment_inline(
&mut self,
block_start: &str,
block_end: &str,
before_char_count: usize,
before_selection: SelectionSetSnapshot,
selections: Vec<Selection>,
primary_index: usize,
) -> Result<CommandResult, CommandError> {
let start_len = block_start.chars().count();
let end_len = block_end.chars().count();
if start_len == 0 || end_len == 0 {
return Ok(CommandResult::Success);
}
let mut selection_ranges: Vec<SearchMatch> = selections
.iter()
.map(|s| self.selection_char_range(s))
.filter(|r| r.start < r.end)
.collect();
if selection_ranges.is_empty() {
return Ok(CommandResult::Success);
}
selection_ranges.sort_by_key(|r| (r.start, r.end));
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TokenOpKind {
Start,
End,
}
struct Op {
start_before: usize,
start_after: usize,
delete_len: usize,
deleted_text: String,
inserted_text: String,
inserted_len: usize,
sel_id: usize,
kind: TokenOpKind,
}
let mut ops: Vec<Op> = Vec::new();
for (sel_id, range) in selection_ranges.iter().enumerate() {
let start = range.start;
let end = range.end;
let already_wrapped = start >= start_len
&& end + end_len <= before_char_count
&& self.editor.text_range(start - start_len, start_len) == block_start
&& self.editor.text_range(end, end_len) == block_end;
if already_wrapped {
let deleted_end = self.editor.text_range(end, end_len);
ops.push(Op {
start_before: end,
start_after: end,
delete_len: end_len,
deleted_text: deleted_end,
inserted_text: String::new(),
inserted_len: 0,
sel_id,
kind: TokenOpKind::End,
});
let start_token_offset = start - start_len;
let deleted_start = self.editor.text_range(start_token_offset, start_len);
ops.push(Op {
start_before: start_token_offset,
start_after: start_token_offset,
delete_len: start_len,
deleted_text: deleted_start,
inserted_text: String::new(),
inserted_len: 0,
sel_id,
kind: TokenOpKind::Start,
});
} else {
ops.push(Op {
start_before: end,
start_after: end,
delete_len: 0,
deleted_text: String::new(),
inserted_text: block_end.to_string(),
inserted_len: end_len,
sel_id,
kind: TokenOpKind::End,
});
ops.push(Op {
start_before: start,
start_after: start,
delete_len: 0,
deleted_text: String::new(),
inserted_text: block_start.to_string(),
inserted_len: start_len,
sel_id,
kind: TokenOpKind::Start,
});
}
}
if ops.is_empty() {
return Ok(CommandResult::Success);
}
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(
"ToggleComment 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 mut new_starts: Vec<usize> = vec![0; selection_ranges.len()];
let mut new_ends: Vec<usize> = vec![0; selection_ranges.len()];
for op in &ops {
match op.kind {
TokenOpKind::Start => {
new_starts[op.sel_id] = if op.inserted_len > 0 {
op.start_after + start_len
} else {
op.start_after
};
}
TokenOpKind::End => {
new_ends[op.sel_id] = op.start_after;
}
}
}
let mut next_selections: Vec<Selection> = Vec::with_capacity(selection_ranges.len());
for i in 0..selection_ranges.len() {
let start = new_starts[i].min(new_ends[i]);
let end = new_starts[i].max(new_ends[i]);
let (start_line, start_col) = self.editor.line_index.char_offset_to_position(start);
let (end_line, end_col) = self.editor.line_index.char_offset_to_position(end);
next_selections.push(Selection {
start: Position::new(start_line, start_col),
end: Position::new(end_line, end_col),
direction: SelectionDirection::Forward,
});
}
self.execute_cursor(CursorCommand::SetSelections {
selections: next_selections,
primary_index: primary_index.min(selection_ranges.len().saturating_sub(1)),
})?;
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_toggle_block_comment_lines(
&mut self,
block_start: &str,
block_end: &str,
before_char_count: usize,
before_selection: SelectionSetSnapshot,
selections: Vec<Selection>,
primary_index: usize,
) -> Result<CommandResult, CommandError> {
let start_len = block_start.chars().count();
let end_len = block_end.chars().count();
if start_len == 0 || end_len == 0 {
return Ok(CommandResult::Success);
}
let mut ranges: Vec<(usize, usize)> = Vec::new();
for sel in &selections {
let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
let start_line = min_pos.line.min(self.editor.line_index.line_count() - 1);
let end_line = max_pos.line.min(self.editor.line_index.line_count() - 1);
let start = self
.editor
.line_index
.position_to_char_offset(start_line, 0);
let end_line_text = self
.editor
.line_index
.get_line_text(end_line)
.unwrap_or_default();
let end = self
.editor
.line_index
.position_to_char_offset(end_line, end_line_text.chars().count());
if start < end {
ranges.push((start, end));
}
}
ranges.sort_unstable();
ranges.dedup();
if ranges.is_empty() {
return Ok(CommandResult::Success);
}
let mut all_wrapped = true;
for (start, end) in &ranges {
if *end < *start + start_len + end_len {
all_wrapped = false;
break;
}
let text = self.editor.text_range(*start, end - start);
if !text.starts_with(block_start) || !text.ends_with(block_end) {
all_wrapped = false;
break;
}
}
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::new();
for (start, end) in &ranges {
if all_wrapped {
let end_token_start = end.saturating_sub(end_len);
let deleted_end = self.editor.text_range(end_token_start, end_len);
ops.push(Op {
start_before: end_token_start,
start_after: end_token_start,
delete_len: end_len,
deleted_text: deleted_end,
inserted_text: String::new(),
inserted_len: 0,
});
let deleted_start = self.editor.text_range(*start, start_len);
ops.push(Op {
start_before: *start,
start_after: *start,
delete_len: start_len,
deleted_text: deleted_start,
inserted_text: String::new(),
inserted_len: 0,
});
} else {
ops.push(Op {
start_before: *end,
start_after: *end,
delete_len: 0,
deleted_text: String::new(),
inserted_text: block_end.to_string(),
inserted_len: end_len,
});
ops.push(Op {
start_before: *start,
start_after: *start,
delete_len: 0,
deleted_text: String::new(),
inserted_text: block_start.to_string(),
inserted_len: start_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(
"ToggleComment 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 (primary_start, primary_end) = ranges
.get(primary_index.min(ranges.len().saturating_sub(1)))
.copied()
.unwrap_or((0, 0));
let caret_offset = primary_end.max(primary_start);
let (line, column) = self.editor.line_index.char_offset_to_position(caret_offset);
let pos = Position::new(line, column);
self.execute_cursor(CursorCommand::SetSelections {
selections: vec![Selection {
start: pos,
end: pos,
direction: SelectionDirection::Forward,
}],
primary_index: 0,
})?;
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)
}
}