fix: replace selected characters with visual r
This commit is contained in:
+118
-21
@@ -325,6 +325,10 @@ enum EditStart {
|
||||
target: char,
|
||||
count: usize,
|
||||
},
|
||||
ReplaceSelection {
|
||||
target: char,
|
||||
selector: SelectionIntent,
|
||||
},
|
||||
Delete,
|
||||
Join,
|
||||
Paste {
|
||||
@@ -559,7 +563,7 @@ core_actions![
|
||||
(
|
||||
"core.replace_character",
|
||||
Action::ReplaceCharacter,
|
||||
&[Mode::Normal]
|
||||
&[Mode::Normal, Mode::Visual]
|
||||
),
|
||||
(
|
||||
"core.substitute_character",
|
||||
@@ -1165,7 +1169,6 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
('C', ChangeToLineEnd),
|
||||
('D', DeleteToLineEnd),
|
||||
('Y', CopyToLineEnd),
|
||||
('r', ReplaceCharacter),
|
||||
('s', SubstituteCharacter),
|
||||
('u', Undo),
|
||||
('U', Redo),
|
||||
@@ -1218,6 +1221,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
('v', Select),
|
||||
('V', SelectLinewise),
|
||||
('.', RepeatLastEdit),
|
||||
('r', ReplaceCharacter),
|
||||
] {
|
||||
bindings
|
||||
.entry(mode)
|
||||
@@ -6217,7 +6221,7 @@ impl Editor {
|
||||
}
|
||||
if let Some((operator, prefix)) = self.line_pending.take() {
|
||||
if operator == 'r' {
|
||||
if self.active.view.mode == Mode::Normal
|
||||
if matches!(self.active.view.mode, Mode::Normal | Mode::Visual)
|
||||
&& let Input::Char(target) = input
|
||||
&& (!target.is_control() || target == '\t')
|
||||
{
|
||||
@@ -6225,14 +6229,24 @@ impl Editor {
|
||||
self.set_message("Diff pane is read-only");
|
||||
} else {
|
||||
let before = self.snapshot();
|
||||
self.replace_characters(target, prefix);
|
||||
self.record_native(
|
||||
before,
|
||||
EditRecipe {
|
||||
start: EditStart::Replace {
|
||||
let (ranges, start) = if self.active.view.mode == Mode::Visual {
|
||||
let ranges = self.selections();
|
||||
let selector = self.selection_intent(&ranges);
|
||||
(ranges, EditStart::ReplaceSelection { target, selector })
|
||||
} else {
|
||||
(
|
||||
self.normal_ranges(Action::SubstituteCharacter, prefix),
|
||||
EditStart::Replace {
|
||||
target,
|
||||
count: prefix,
|
||||
},
|
||||
)
|
||||
};
|
||||
self.replace_characters(target, &ranges);
|
||||
self.record_native(
|
||||
before,
|
||||
EditRecipe {
|
||||
start,
|
||||
steps: Vec::new(),
|
||||
},
|
||||
);
|
||||
@@ -8462,8 +8476,7 @@ impl Editor {
|
||||
self.reveal_caret();
|
||||
}
|
||||
|
||||
fn replace_characters(&mut self, replacement: char, count: usize) {
|
||||
let ranges = self.normal_ranges(Action::SubstituteCharacter, count);
|
||||
fn replace_characters(&mut self, replacement: char, ranges: &[Selection]) {
|
||||
let offsets = line_offsets(&self.active.lines);
|
||||
let mut bytes = [0; 4];
|
||||
let replacement: &str = replacement.encode_utf8(&mut bytes);
|
||||
@@ -8471,20 +8484,52 @@ impl Editor {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, range)| range.start != range.end)
|
||||
.map(|(target, range)| Edit {
|
||||
target: Some(target),
|
||||
start: cursor_offset(&self.active.lines, &offsets, range.start),
|
||||
end: cursor_offset(&self.active.lines, &offsets, range.end),
|
||||
text: if range.end.column - range.start.column == 1 {
|
||||
Cow::Borrowed(replacement)
|
||||
.map(|(target, range)| {
|
||||
let start = cursor_offset(&self.active.lines, &offsets, range.start);
|
||||
let end = cursor_offset(&self.active.lines, &offsets, range.end);
|
||||
let text = if range.start.row == range.end.row {
|
||||
let count = range.end.column - range.start.column;
|
||||
if count == 1 {
|
||||
Cow::Borrowed(replacement)
|
||||
} else {
|
||||
Cow::Owned(replacement.repeat(count))
|
||||
}
|
||||
} else {
|
||||
Cow::Owned(replacement.repeat(range.end.column - range.start.column))
|
||||
},
|
||||
caret: 0,
|
||||
round_up: false,
|
||||
let mut text = String::with_capacity(end - start);
|
||||
for row in range.start.row..=range.end.row {
|
||||
let first = if row == range.start.row {
|
||||
range.start.column
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let last = if row == range.end.row {
|
||||
range.end.column
|
||||
} else {
|
||||
grapheme_count(&self.active.lines[row])
|
||||
};
|
||||
text.reserve((last - first) * replacement.len());
|
||||
for _ in first..last {
|
||||
text.push_str(replacement);
|
||||
}
|
||||
if row < range.end.row {
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
Cow::Owned(text)
|
||||
};
|
||||
Edit {
|
||||
target: Some(target),
|
||||
start,
|
||||
end,
|
||||
text,
|
||||
caret: 0,
|
||||
round_up: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.apply_edits(edits);
|
||||
self.active.view.visual_motions = None;
|
||||
self.active.view.linewise = false;
|
||||
self.active.view.mode = Mode::Normal;
|
||||
self.normalize_targets();
|
||||
}
|
||||
@@ -9021,10 +9066,18 @@ impl Editor {
|
||||
EditStart::Selection {
|
||||
selector: SelectionIntent::Spans(spans),
|
||||
..
|
||||
}
|
||||
| EditStart::ReplaceSelection {
|
||||
selector: SelectionIntent::Spans(spans),
|
||||
..
|
||||
} if self.active.view.mode != Mode::Visual => Some(spans.len()),
|
||||
EditStart::Selection {
|
||||
selector: SelectionIntent::Rows(rows),
|
||||
..
|
||||
}
|
||||
| EditStart::ReplaceSelection {
|
||||
selector: SelectionIntent::Rows(rows),
|
||||
..
|
||||
} if self.active.view.mode != Mode::Visual => Some(rows.len()),
|
||||
_ => None,
|
||||
};
|
||||
@@ -9117,7 +9170,14 @@ impl Editor {
|
||||
self.normalize_targets();
|
||||
}
|
||||
}
|
||||
EditStart::Replace { target, count } => self.replace_characters(*target, *count),
|
||||
EditStart::Replace { target, count } => {
|
||||
let ranges = self.normal_ranges(Action::SubstituteCharacter, *count);
|
||||
self.replace_characters(*target, &ranges);
|
||||
}
|
||||
EditStart::ReplaceSelection { target, selector } => {
|
||||
let ranges = self.replay_ranges(selector);
|
||||
self.replace_characters(*target, &ranges);
|
||||
}
|
||||
EditStart::Lines { change, count } => {
|
||||
let ranges = self.line_selections(*count);
|
||||
clipboard = Some((self.selection_fragments(&ranges), true));
|
||||
@@ -19937,6 +19997,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_visual_replacement_preserves_snapshot_selections_and_history() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["one two three".into()],
|
||||
vec!["four five six".into()],
|
||||
);
|
||||
for focus in [Focus::Before, Focus::After] {
|
||||
if editor.diff().unwrap().focus != focus {
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
type_text(&mut editor, "vl");
|
||||
let text = editor.context().text;
|
||||
let targets = editor.active.view.targets.clone();
|
||||
let undo = editor.active.undo.len();
|
||||
let clipboard = editor.clipboard.clone();
|
||||
for pending in [false, true] {
|
||||
if pending {
|
||||
editor.line_pending = Some(('r', 1));
|
||||
} else {
|
||||
editor.handle(Input::Char('r'));
|
||||
}
|
||||
editor.handle(Input::Char('X'));
|
||||
assert_eq!(editor.context().text, text);
|
||||
assert_eq!(editor.active.view.targets, targets);
|
||||
assert_eq!(editor.mode(), Mode::Visual);
|
||||
assert_eq!(editor.line_operator_pending(), None);
|
||||
assert_eq!(editor.clipboard, clipboard);
|
||||
assert_eq!(editor.active.undo.len(), undo);
|
||||
}
|
||||
editor.handle(Input::Escape);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_carets(editor: &mut Editor, positions: &[(usize, usize)], primary: usize) {
|
||||
editor.active.view.mode = Mode::Normal;
|
||||
editor.active.view.visual_motions = None;
|
||||
|
||||
Reference in New Issue
Block a user