mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 01:34:02 +00:00
parent
c2c04616b4
commit
20f98e4d17
19 changed files with 544 additions and 40 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -8767,12 +8767,14 @@ dependencies = [
|
|||
"collections",
|
||||
"command_palette",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"language",
|
||||
"language_selector",
|
||||
"log",
|
||||
"lsp",
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
"project",
|
||||
|
|
|
@ -316,6 +316,7 @@
|
|||
{
|
||||
"context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
|
||||
"bindings": {
|
||||
".": "vim::Repeat",
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
|
@ -331,10 +332,7 @@
|
|||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
],
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
|
|
|
@ -2269,10 +2269,6 @@ impl Editor {
|
|||
if self.read_only {
|
||||
return;
|
||||
}
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text });
|
||||
return;
|
||||
}
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut brace_inserted = false;
|
||||
|
@ -3207,17 +3203,30 @@ impl Editor {
|
|||
.count();
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let mut range_to_replace: Option<Range<isize>> = None;
|
||||
let mut ranges = Vec::new();
|
||||
for selection in &selections {
|
||||
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
|
||||
let start = selection.start.saturating_sub(lookbehind);
|
||||
let end = selection.end + lookahead;
|
||||
if selection.id == newest_selection.id {
|
||||
range_to_replace = Some(
|
||||
((start + common_prefix_len) as isize - selection.start as isize)
|
||||
..(end as isize - selection.start as isize),
|
||||
);
|
||||
}
|
||||
ranges.push(start + common_prefix_len..end);
|
||||
} else {
|
||||
common_prefix_len = 0;
|
||||
ranges.clear();
|
||||
ranges.extend(selections.iter().map(|s| {
|
||||
if s.id == newest_selection.id {
|
||||
range_to_replace = Some(
|
||||
old_range.start.to_offset_utf16(&snapshot).0 as isize
|
||||
- selection.start as isize
|
||||
..old_range.end.to_offset_utf16(&snapshot).0 as isize
|
||||
- selection.start as isize,
|
||||
);
|
||||
old_range.clone()
|
||||
} else {
|
||||
s.start..s.end
|
||||
|
@ -3228,6 +3237,11 @@ impl Editor {
|
|||
}
|
||||
let text = &text[common_prefix_len..];
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = text.to_string();
|
||||
|
@ -3685,6 +3699,10 @@ impl Editor {
|
|||
|
||||
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
|
||||
}
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: suggestion.text.to_string().into(),
|
||||
});
|
||||
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
|
||||
cx.notify();
|
||||
true
|
||||
|
@ -8436,6 +8454,41 @@ impl Editor {
|
|||
pub fn inlay_hint_cache(&self) -> &InlayHintCache {
|
||||
&self.inlay_hint_cache
|
||||
}
|
||||
|
||||
pub fn replay_insert_event(
|
||||
&mut self,
|
||||
text: &str,
|
||||
relative_utf16_range: Option<Range<isize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
if let Some(relative_utf16_range) = relative_utf16_range {
|
||||
let selections = self.selections.all::<OffsetUtf16>(cx);
|
||||
self.change_selections(None, cx, |s| {
|
||||
let new_ranges = selections.into_iter().map(|range| {
|
||||
let start = OffsetUtf16(
|
||||
range
|
||||
.head()
|
||||
.0
|
||||
.saturating_add_signed(relative_utf16_range.start),
|
||||
);
|
||||
let end = OffsetUtf16(
|
||||
range
|
||||
.head()
|
||||
.0
|
||||
.saturating_add_signed(relative_utf16_range.end),
|
||||
);
|
||||
start..end
|
||||
});
|
||||
s.select_ranges(new_ranges);
|
||||
});
|
||||
}
|
||||
|
||||
self.handle_input(text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn document_to_inlay_range(
|
||||
|
@ -8524,6 +8577,10 @@ pub enum Event {
|
|||
InputIgnored {
|
||||
text: Arc<str>,
|
||||
},
|
||||
InputHandled {
|
||||
utf16_range_to_replace: Option<Range<isize>>,
|
||||
text: Arc<str>,
|
||||
},
|
||||
ExcerptsAdded {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
predecessor: ExcerptId,
|
||||
|
@ -8744,29 +8801,51 @@ impl View for Editor {
|
|||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
if this.input_enabled {
|
||||
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
|
||||
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
|
||||
Some(this.selection_replacement_ranges(range_utf16, cx))
|
||||
} else {
|
||||
this.marked_text_ranges(cx)
|
||||
};
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(new_selected_ranges) = new_selected_ranges {
|
||||
this.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(new_selected_ranges)
|
||||
});
|
||||
}
|
||||
self.transact(cx, |this, cx| {
|
||||
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
|
||||
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
|
||||
Some(this.selection_replacement_ranges(range_utf16, cx))
|
||||
} else {
|
||||
this.marked_text_ranges(cx)
|
||||
};
|
||||
|
||||
let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
|
||||
let newest_selection_id = this.selections.newest_anchor().id;
|
||||
this.selections
|
||||
.all::<OffsetUtf16>(cx)
|
||||
.iter()
|
||||
.zip(ranges_to_replace.iter())
|
||||
.find_map(|(selection, range)| {
|
||||
if selection.id == newest_selection_id {
|
||||
Some(
|
||||
(range.start.0 as isize - selection.head().0 as isize)
|
||||
..(range.end.0 as isize - selection.head().0 as isize),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
||||
if let Some(new_selected_ranges) = new_selected_ranges {
|
||||
this.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(new_selected_ranges)
|
||||
});
|
||||
}
|
||||
|
||||
this.handle_input(text, cx);
|
||||
});
|
||||
|
||||
if !self.input_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(transaction) = self.ime_transaction {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.group_until_transaction(transaction, cx);
|
||||
|
@ -8784,6 +8863,7 @@ impl View for Editor {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -8808,6 +8888,29 @@ impl View for Editor {
|
|||
None
|
||||
};
|
||||
|
||||
let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
|
||||
let newest_selection_id = this.selections.newest_anchor().id;
|
||||
this.selections
|
||||
.all::<OffsetUtf16>(cx)
|
||||
.iter()
|
||||
.zip(ranges_to_replace.iter())
|
||||
.find_map(|(selection, range)| {
|
||||
if selection.id == newest_selection_id {
|
||||
Some(
|
||||
(range.start.0 as isize - selection.head().0 as isize)
|
||||
..(range.end.0 as isize - selection.head().0 as isize),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
||||
if let Some(ranges) = ranges_to_replace {
|
||||
this.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
}
|
||||
|
|
|
@ -7807,7 +7807,7 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
|
|||
/// Handle completion request passing a marked string specifying where the completion
|
||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
/// should be returned using '<' and '>' to delimit the range
|
||||
fn handle_completion_request<'a>(
|
||||
pub fn handle_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
|
|
|
@ -1110,7 +1110,7 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.is_fullscreen
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
|
||||
pub fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
|
||||
if let Some(view_id) = view_id {
|
||||
self.halt_action_dispatch = false;
|
||||
self.visit_dispatch_path(view_id, |view_id, capture_phase, cx| {
|
||||
|
|
|
@ -2,13 +2,13 @@ Design notes:
|
|||
|
||||
This crate is split into two conceptual halves:
|
||||
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
|
||||
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
|
||||
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
|
||||
|
||||
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
|
||||
|
||||
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
|
||||
|
||||
#Input
|
||||
#Input
|
||||
|
||||
There are currently many distinct paths for getting keystrokes to the terminal:
|
||||
|
||||
|
@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal:
|
|||
|
||||
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
|
||||
|
||||
4. Pasted text has a separate pathway.
|
||||
4. Pasted text has a separate pathway.
|
||||
|
||||
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
|
||||
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
|
||||
|
|
|
@ -38,6 +38,7 @@ language_selector = { path = "../language_selector"}
|
|||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
@ -47,3 +48,4 @@ util = { path = "../util", features = ["test-support"] }
|
|||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
|
|
|
@ -34,6 +34,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
|||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.recording = false;
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
vim.active_editor = None;
|
||||
|
|
|
@ -11,8 +11,9 @@ pub fn init(cx: &mut AppContext) {
|
|||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
|
@ -20,7 +21,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
|||
});
|
||||
});
|
||||
});
|
||||
state.switch_mode(Mode::Normal, false, cx);
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ mod case;
|
|||
mod change;
|
||||
mod delete;
|
||||
mod paste;
|
||||
mod repeat;
|
||||
mod scroll;
|
||||
mod search;
|
||||
pub mod substitute;
|
||||
|
@ -34,6 +35,7 @@ actions!(
|
|||
vim,
|
||||
[
|
||||
InsertAfter,
|
||||
InsertBefore,
|
||||
InsertFirstNonWhitespace,
|
||||
InsertEndOfLine,
|
||||
InsertLineAbove,
|
||||
|
@ -48,28 +50,37 @@ actions!(
|
|||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
paste::init(cx);
|
||||
repeat::init(cx);
|
||||
scroll::init(cx);
|
||||
search::init(cx);
|
||||
substitute::init(cx);
|
||||
|
||||
cx.add_action(insert_after);
|
||||
cx.add_action(insert_before);
|
||||
cx.add_action(insert_first_non_whitespace);
|
||||
cx.add_action(insert_end_of_line);
|
||||
cx.add_action(insert_line_above);
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(change_case);
|
||||
substitute::init(cx);
|
||||
search::init(cx);
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action();
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::Left, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action();
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::Right, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
let times = vim.pop_number_operator(cx);
|
||||
change_motion(
|
||||
vim,
|
||||
|
@ -83,6 +94,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action();
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(
|
||||
vim,
|
||||
|
@ -94,8 +106,6 @@ pub fn init(cx: &mut AppContext) {
|
|||
);
|
||||
})
|
||||
});
|
||||
scroll::init(cx);
|
||||
paste::init(cx);
|
||||
}
|
||||
|
||||
pub fn normal_motion(
|
||||
|
@ -151,6 +161,7 @@ fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut Win
|
|||
|
||||
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
|
@ -162,12 +173,20 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
|||
});
|
||||
}
|
||||
|
||||
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_first_non_whitespace(
|
||||
_: &mut Workspace,
|
||||
_: &InsertFirstNonWhitespace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
|
@ -184,6 +203,7 @@ fn insert_first_non_whitespace(
|
|||
|
||||
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
|
@ -197,6 +217,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
|||
|
||||
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
|
@ -229,6 +250,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
|||
|
||||
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording();
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
|
@ -260,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
|||
|
||||
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
|
|||
|
||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action();
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
|
|
|
@ -4,6 +4,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
|
|||
use gpui::WindowContext;
|
||||
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
@ -37,6 +38,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
|||
}
|
||||
|
||||
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
|
|
@ -28,6 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
|||
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
|
200
crates/vim/src/normal/repeat.rs
Normal file
200
crates/vim/src/normal/repeat.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
use crate::{
|
||||
state::{Mode, ReplayableAction},
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, AppContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat,]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.replaying = false;
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
});
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.repeat_actions.clone();
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Some(new_count) = vim.pop_number_operator(cx) {
|
||||
vim.workspace_state.recorded_count = Some(new_count);
|
||||
}
|
||||
vim.workspace_state.replaying = true;
|
||||
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed")),
|
||||
ReplayableAction::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => editor.update(&mut cx, |editor, cx| {
|
||||
editor.replay_insert_event(
|
||||
&text,
|
||||
utf16_range_to_replace.clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
}?
|
||||
}
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
|
||||
use gpui::{executor::Deterministic, View};
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// "o"
|
||||
cx.set_shared_state("ˇhello").await;
|
||||
cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state("hello\nworlˇd").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.assert_shared_state("hello\nworld\nworlˇd").await;
|
||||
|
||||
// "d"
|
||||
cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
|
||||
cx.simulate_shared_keystrokes(["g", "g", "."]).await;
|
||||
cx.assert_shared_state("ˇ\nworld\nrld").await;
|
||||
|
||||
// "p" (note that it pastes the current clipboard)
|
||||
cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
|
||||
cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
|
||||
.await;
|
||||
cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
|
||||
|
||||
// "~" (note that counts apply to the action taken, not . itself)
|
||||
cx.set_shared_state("ˇthe quick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["2", "~", "."]).await;
|
||||
cx.set_shared_state("THE ˇquick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["3", "."]).await;
|
||||
cx.set_shared_state("THE QUIˇck brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.set_shared_state("THE QUICK ˇbrown fox").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("hˇllo", Mode::Normal);
|
||||
cx.simulate_keystrokes(["i"]);
|
||||
|
||||
// simulate brazilian input for ä.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
|
||||
editor.replace_text_in_range(None, "ä", cx);
|
||||
});
|
||||
cx.simulate_keystrokes(["escape"]);
|
||||
cx.assert_state("hˇällo", Mode::Normal);
|
||||
cx.simulate_keystrokes(["."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hˇäällo", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_completion(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let mut cx = VimTestContext::new_with_lsp(cx, true);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
onˇe
|
||||
two
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
let mut request =
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
|
||||
let position = params.text_document_position.position;
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(position.clone(), position.clone()),
|
||||
new_text: "first".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "second".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(position.clone(), position.clone()),
|
||||
new_text: "second".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.simulate_keystrokes(["a", "."]);
|
||||
request.next().await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.secondˇ!
|
||||
two
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["j", "."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.second!
|
||||
two.secondˇ!
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
use gpui::keymap_matcher::KeymapContext;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{keymap_matcher::KeymapContext, Action};
|
||||
use language::CursorShape;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::searchable::Direction;
|
||||
|
@ -52,6 +54,36 @@ pub struct EditorState {
|
|||
pub struct WorkspaceState {
|
||||
pub search: SearchState,
|
||||
pub last_find: Option<Motion>,
|
||||
|
||||
pub recording: bool,
|
||||
pub stop_recording_after_next_action: bool,
|
||||
pub replaying: bool,
|
||||
pub recorded_count: Option<usize>,
|
||||
pub repeat_actions: Vec<ReplayableAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReplayableAction {
|
||||
Action(Box<dyn Action>),
|
||||
Insertion {
|
||||
text: Arc<str>,
|
||||
utf16_range_to_replace: Option<Range<isize>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for ReplayableAction {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Action(action) => Self::Action(action.boxed_clone()),
|
||||
Self::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => Self::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: utf16_range_to_replace.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
|
|||
use editor::test::{
|
||||
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
|
||||
};
|
||||
use futures::Future;
|
||||
use gpui::ContextHandle;
|
||||
use lsp::request;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
@ -124,6 +126,19 @@ impl<'a> VimTestContext<'a> {
|
|||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
self.cx.handle_request::<T, F, Fut>(handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestContext<'a> {
|
||||
|
|
|
@ -25,10 +25,12 @@ use normal::normal_replace;
|
|||
use serde::Deserialize;
|
||||
use settings::{Setting, SettingsStore};
|
||||
use state::{EditorState, Mode, Operator, WorkspaceState};
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use visual::{visual_block_motion, visual_replace};
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
use crate::state::ReplayableAction;
|
||||
|
||||
struct VimModeSetting(bool);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
|
@ -102,6 +104,19 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
|||
return true;
|
||||
}
|
||||
if let Some(handled_by) = handled_by {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.repeat_actions
|
||||
.push(ReplayableAction::Action(handled_by.boxed_clone()));
|
||||
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keystroke is handled by the vim system, so continue forward
|
||||
if handled_by.namespace() == "vim" {
|
||||
return true;
|
||||
|
@ -156,7 +171,12 @@ impl Vim {
|
|||
}
|
||||
Event::InputIgnored { text } => {
|
||||
Vim::active_editor_input_ignored(text.clone(), cx);
|
||||
Vim::record_insertion(text, None, cx)
|
||||
}
|
||||
Event::InputHandled {
|
||||
text,
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
|
||||
_ => {}
|
||||
}));
|
||||
|
||||
|
@ -176,6 +196,27 @@ impl Vim {
|
|||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn record_insertion(
|
||||
text: &Arc<str>,
|
||||
range_to_replace: Option<Range<isize>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.repeat_actions
|
||||
.push(ReplayableAction::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
});
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn update_active_editor<S>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -184,6 +225,36 @@ impl Vim {
|
|||
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
||||
Some(editor.update(cx, update))
|
||||
}
|
||||
// ~, shift-j, x, shift-x, p
|
||||
// shift-c, shift-d, shift-i, i, a, o, shift-o, s
|
||||
// c, d
|
||||
// r
|
||||
|
||||
// TODO: shift-j?
|
||||
//
|
||||
pub fn start_recording(&mut self) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
self.workspace_state.repeat_actions = Default::default();
|
||||
self.workspace_state.recorded_count =
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
Some(number)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.stop_recording_after_next_action = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_current_action(&mut self) {
|
||||
self.start_recording();
|
||||
self.stop_recording();
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
|
||||
let state = self.state();
|
||||
|
@ -247,6 +318,12 @@ impl Vim {
|
|||
}
|
||||
|
||||
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||
if matches!(
|
||||
operator,
|
||||
Operator::Change | Operator::Delete | Operator::Replace
|
||||
) {
|
||||
self.start_recording()
|
||||
};
|
||||
self.update_state(|state| state.operator_stack.push(operator));
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
@ -272,6 +349,12 @@ impl Vim {
|
|||
}
|
||||
|
||||
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
if let Some(number) = self.workspace_state.recorded_count {
|
||||
return Some(number);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
return Some(number);
|
||||
|
|
|
@ -277,6 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
|
|||
|
||||
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let line_mode = editor.selections.line_mode;
|
||||
|
@ -339,6 +340,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
|
|||
|
||||
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
|
||||
|
|
38
crates/vim/test_data/test_dot_repeat.json
Normal file
38
crates/vim/test_data/test_dot_repeat.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{"Put":{"state":"ˇhello"}}
|
||||
{"Key":"o"}
|
||||
{"Key":"w"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"l"}
|
||||
{"Key":"d"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"hello\nworlˇd","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hello\nworld\nworlˇd","mode":"Normal"}}
|
||||
{"Key":"^"}
|
||||
{"Key":"d"}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ˇ\nworld\nrld","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"y"}
|
||||
{"Key":"y"}
|
||||
{"Key":"p"}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"y"}
|
||||
{"Key":"y"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"\nworld\nworld\nrld\nˇrld","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇthe quick brown fox"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"~"}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE ˇquick brown fox"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE QUIˇck brown fox"}}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE QUICK ˇbrown fox"}}
|
Loading…
Reference in a new issue