mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
Remove 2 suffix for vim, diagnostics, go_to_line, theme_selector, command_palette, file_finder
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
37e6533b28
commit
252694390a
134
Cargo.lock
generated
134
Cargo.lock
generated
@ -1906,27 +1906,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "command_palette"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed-actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command_palette2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
@ -1934,7 +1913,7 @@ dependencies = [
|
||||
"editor2",
|
||||
"env_logger",
|
||||
"fuzzy2",
|
||||
"go_to_line2",
|
||||
"go_to_line",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"menu2",
|
||||
@ -2623,33 +2602,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "diagnostics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"postage",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diagnostics2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
@ -3166,29 +3118,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "file_finder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"postage",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file_finder2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"ctor",
|
||||
@ -3745,21 +3674,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "go_to_line"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"menu",
|
||||
"postage",
|
||||
"settings",
|
||||
"text",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "go_to_line2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor2",
|
||||
"gpui2",
|
||||
@ -10540,40 +10454,6 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"diagnostics",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
"language_selector",
|
||||
"log",
|
||||
"lsp",
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
"project",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"tokio",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed-actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vim2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compat",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"command_palette2",
|
||||
"diagnostics2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
@ -11007,7 +10887,7 @@ dependencies = [
|
||||
"theme_selector",
|
||||
"ui2",
|
||||
"util",
|
||||
"vim2",
|
||||
"vim",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
@ -11429,21 +11309,21 @@ dependencies = [
|
||||
"client2",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette2",
|
||||
"command_palette",
|
||||
"copilot2",
|
||||
"copilot_button2",
|
||||
"ctor",
|
||||
"db2",
|
||||
"diagnostics2",
|
||||
"diagnostics",
|
||||
"editor2",
|
||||
"env_logger",
|
||||
"feature_flags2",
|
||||
"feedback2",
|
||||
"file_finder2",
|
||||
"file_finder",
|
||||
"fs2",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"go_to_line2",
|
||||
"go_to_line",
|
||||
"gpui2",
|
||||
"ignore",
|
||||
"image",
|
||||
@ -11530,7 +11410,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"vim2",
|
||||
"vim",
|
||||
"welcome",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
|
@ -24,7 +24,6 @@ members = [
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
"crates/command_palette",
|
||||
"crates/command_palette2",
|
||||
"crates/component_test",
|
||||
"crates/context_menu",
|
||||
"crates/copilot",
|
||||
@ -35,7 +34,6 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/diagnostics",
|
||||
"crates/diagnostics2",
|
||||
"crates/drag_and_drop",
|
||||
"crates/editor",
|
||||
"crates/feature_flags",
|
||||
@ -49,7 +47,6 @@ members = [
|
||||
"crates/fuzzy2",
|
||||
"crates/git",
|
||||
"crates/go_to_line",
|
||||
"crates/go_to_line2",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui2",
|
||||
|
@ -10,23 +10,28 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
zed-actions = { path = "../zed-actions" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package="workspace2", path = "../workspace2" }
|
||||
zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package="language2", path = "../language2", features = ["test-support"] }
|
||||
project = { package="project2", path = "../project2", features = ["test-support"] }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
go_to_line = { path = "../go_to_line" }
|
||||
serde_json.workspace = true
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
|
@ -1,26 +1,92 @@
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle,
|
||||
AppContext, Element, MouseState, ViewContext,
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::cmp::{self, Reverse};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(toggle_command_palette);
|
||||
CommandPalette::init(cx);
|
||||
}
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
|
||||
pub type CommandPalette = Picker<CommandPaletteDelegate>;
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(HitCounts::default());
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.observe_new_views(CommandPalette::register).detach();
|
||||
}
|
||||
|
||||
impl ModalView for CommandPalette {}
|
||||
|
||||
pub struct CommandPalette {
|
||||
picker: View<Picker<CommandPaletteDelegate>>,
|
||||
}
|
||||
|
||||
impl CommandPalette {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||
let Some(previous_focus_handle) = cx.focused() else {
|
||||
return;
|
||||
};
|
||||
workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx));
|
||||
});
|
||||
}
|
||||
|
||||
fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext<Self>) -> Self {
|
||||
let filter = cx.try_global::<CommandPaletteFilter>();
|
||||
|
||||
let commands = cx
|
||||
.available_actions()
|
||||
.into_iter()
|
||||
.filter_map(|action| {
|
||||
let name = action.name();
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
|| f.hidden_action_types.contains(&action.type_id())
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(&name),
|
||||
action,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let delegate =
|
||||
CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
|
||||
|
||||
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||
|
||||
impl FocusableView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommandPalette {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CommandPaletteInterceptor =
|
||||
Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
|
||||
@ -32,24 +98,26 @@ pub struct CommandInterceptResult {
|
||||
}
|
||||
|
||||
pub struct CommandPaletteDelegate {
|
||||
actions: Vec<Command>,
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
all_commands: Vec<Command>,
|
||||
commands: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
focused_view_id: usize,
|
||||
previous_focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
Confirmed {
|
||||
window: AnyWindowHandle,
|
||||
focused_view_id: usize,
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
}
|
||||
struct Command {
|
||||
name: String,
|
||||
action: Box<dyn Action>,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl Clone for Command {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
action: self.action.boxed_clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hit count for each command in the palette.
|
||||
@ -58,26 +126,27 @@ struct Command {
|
||||
#[derive(Default)]
|
||||
struct HitCounts(HashMap<String, usize>);
|
||||
|
||||
fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx))
|
||||
});
|
||||
}
|
||||
|
||||
impl CommandPaletteDelegate {
|
||||
pub fn new(focused_view_id: usize) -> Self {
|
||||
fn new(
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
commands: Vec<Command>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
actions: Default::default(),
|
||||
command_palette,
|
||||
all_commands: commands.clone(),
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
focused_view_id,
|
||||
previous_focus_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
fn placeholder_text(&self) -> std::sync::Arc<str> {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Execute a command...".into()
|
||||
}
|
||||
|
||||
@ -98,49 +167,20 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let view_id = self.focused_view_id;
|
||||
let window = cx.window();
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let mut actions = window
|
||||
.available_actions(view_id, &cx)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|(name, action, bindings)| {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.hidden_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
let mut commands = self.all_commands.clone();
|
||||
|
||||
if filtered {
|
||||
None
|
||||
} else {
|
||||
Some(Command {
|
||||
name: humanize_action_name(name),
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut actions = cx.read(move |cx| {
|
||||
let hit_counts = cx.optional_global::<HitCounts>();
|
||||
actions.sort_by_key(|action| {
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
cx.read_global::<HitCounts, _>(|hit_counts, _| {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
Reverse(hit_counts.and_then(|map| map.0.get(&action.name)).cloned()),
|
||||
Reverse(hit_counts.0.get(&action.name).cloned()),
|
||||
action.name.clone(),
|
||||
)
|
||||
});
|
||||
actions
|
||||
});
|
||||
let candidates = actions
|
||||
})
|
||||
.ok();
|
||||
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
@ -167,17 +207,17 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
cx.background(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
let mut intercept_result = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteInterceptor>() {
|
||||
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let mut intercept_result = cx
|
||||
.try_read_global(|interceptor: &CommandPaletteInterceptor, cx| {
|
||||
(interceptor)(&query, cx)
|
||||
})
|
||||
.flatten();
|
||||
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
@ -187,6 +227,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
@ -195,29 +236,29 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
{
|
||||
if let Some(idx) = matches
|
||||
.iter()
|
||||
.position(|m| actions[m.candidate_id].action.id() == action.id())
|
||||
.position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
|
||||
{
|
||||
matches.remove(idx);
|
||||
}
|
||||
actions.push(Command {
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
keystrokes: vec![],
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
StringMatch {
|
||||
candidate_id: actions.len() - 1,
|
||||
candidate_id: commands.len() - 1,
|
||||
string,
|
||||
positions,
|
||||
score: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.actions = actions;
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.commands = commands;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_ix = 0;
|
||||
@ -230,83 +271,60 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if !self.matches.is_empty() {
|
||||
let window = cx.window();
|
||||
let focused_view_id = self.focused_view_id;
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(cx);
|
||||
return;
|
||||
}
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.actions.remove(action_ix);
|
||||
cx.update_default_global(|hit_counts: &mut HitCounts, _| {
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
cx.update_global(|hit_counts: &mut HitCounts, _| {
|
||||
*hit_counts.0.entry(command.name).or_default() += 1;
|
||||
});
|
||||
let action = command.action;
|
||||
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
window
|
||||
.dispatch_action(focused_view_id, action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow!("window was closed"))
|
||||
})
|
||||
cx.focus(&self.previous_focus_handle);
|
||||
cx.window_context()
|
||||
.spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
self.dismissed(cx);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let mat = &self.matches[ix];
|
||||
let command = &self.actions[mat.candidate_id];
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
let key_style = &theme.command_palette.key.in_state(selected);
|
||||
let keystroke_spacing = theme.command_palette.keystroke_spacing;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(mat.string.clone(), style.label.clone())
|
||||
.with_highlights(mat.positions.clone()),
|
||||
)
|
||||
.with_children(command.keystrokes.iter().map(|keystroke| {
|
||||
Flex::row()
|
||||
.with_children(
|
||||
[
|
||||
(keystroke.ctrl, "^"),
|
||||
(keystroke.alt, "⌥"),
|
||||
(keystroke.cmd, "⌘"),
|
||||
(keystroke.shift, "⇧"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(modifier, label)| {
|
||||
if modifier {
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
Some(
|
||||
Label::new(label, key_style.label.clone())
|
||||
.contained()
|
||||
.with_style(key_style.container),
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
&self.previous_focus_handle,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(keystroke.key.clone(), key_style.label.clone())
|
||||
.contained()
|
||||
.with_style(key_style.container),
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(keystroke_spacing)
|
||||
.flex_float()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@ -338,8 +356,7 @@ impl std::fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command")
|
||||
.field("name", &self.name)
|
||||
.field("keystrokes", &self.keystrokes)
|
||||
.finish()
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,7 +366,9 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use go_to_line::GoToLine;
|
||||
use gpui::TestAppContext;
|
||||
use language::Point;
|
||||
use project::Project;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
@ -370,101 +389,121 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let workspace = window.root(cx);
|
||||
let editor = window.add_view(cx, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_text("abc", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
cx.focus(&editor);
|
||||
workspace.add_item(Box::new(editor.clone()), cx)
|
||||
workspace.add_item(Box::new(editor.clone()), cx);
|
||||
editor.update(cx, |editor, cx| editor.focus(cx))
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
toggle_command_palette(workspace, &Toggle, cx);
|
||||
});
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
|
||||
let palette = workspace.read_with(cx, |workspace, _| {
|
||||
workspace.modal::<CommandPalette>().unwrap()
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
});
|
||||
|
||||
palette
|
||||
.update(cx, |palette, cx| {
|
||||
// Fill up palette's command list by running an empty query;
|
||||
// we only need it to subsequently assert that the palette is initially
|
||||
// sorted by command's name.
|
||||
palette.delegate_mut().update_matches("".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
palette.update(cx, |palette, _| {
|
||||
assert!(palette.delegate.commands.len() > 5);
|
||||
let is_sorted =
|
||||
|actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
|
||||
assert!(is_sorted(&palette.delegate().actions));
|
||||
assert!(is_sorted(&palette.delegate.commands));
|
||||
});
|
||||
|
||||
palette
|
||||
.update(cx, |palette, cx| {
|
||||
palette
|
||||
.delegate_mut()
|
||||
.update_matches("bcksp".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
cx.simulate_input("bcksp");
|
||||
|
||||
palette.update(cx, |palette, cx| {
|
||||
assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
|
||||
palette.confirm(&Default::default(), cx);
|
||||
palette.update(cx, |palette, _| {
|
||||
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
editor.read_with(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "ab");
|
||||
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
|
||||
assert_eq!(editor.read(cx).text(cx), "ab")
|
||||
});
|
||||
|
||||
// Add namespace filter, and redeploy the palette
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
toggle_command_palette(workspace, &Toggle, cx);
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_input("bcksp");
|
||||
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
});
|
||||
|
||||
// Assert editor command not present
|
||||
let palette = workspace.read_with(cx, |workspace, _| {
|
||||
workspace.modal::<CommandPalette>().unwrap()
|
||||
});
|
||||
|
||||
palette
|
||||
.update(cx, |palette, cx| {
|
||||
palette
|
||||
.delegate_mut()
|
||||
.update_matches("bcksp".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
palette.update(cx, |palette, _| {
|
||||
assert!(palette.delegate().matches.is_empty())
|
||||
assert!(palette.delegate.matches.is_empty())
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_line(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-n");
|
||||
|
||||
let editor = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
});
|
||||
editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_input("go to line: Toggle");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.active_modal::<GoToLine>(cx).is_some())
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("3 enter");
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(editor.focus_handle(cx).is_focused(cx));
|
||||
assert_eq!(
|
||||
editor.selections.last::<Point>(cx).range().start,
|
||||
Point::new(2, 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init((), cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
menu::init();
|
||||
go_to_line::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
Project::init_settings(cx);
|
||||
settings::load_default_keymap(cx);
|
||||
app_state
|
||||
})
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
[package]
|
||||
name = "command_palette2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/command_palette.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package="workspace2", path = "../workspace2" }
|
||||
zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
[dev-dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package="language2", path = "../language2", features = ["test-support"] }
|
||||
project = { package="project2", path = "../project2", features = ["test-support"] }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
|
||||
serde_json.workspace = true
|
||||
workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
@ -1,510 +0,0 @@
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(HitCounts::default());
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.observe_new_views(CommandPalette::register).detach();
|
||||
}
|
||||
|
||||
impl ModalView for CommandPalette {}
|
||||
|
||||
pub struct CommandPalette {
|
||||
picker: View<Picker<CommandPaletteDelegate>>,
|
||||
}
|
||||
|
||||
impl CommandPalette {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||
let Some(previous_focus_handle) = cx.focused() else {
|
||||
return;
|
||||
};
|
||||
workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx));
|
||||
});
|
||||
}
|
||||
|
||||
fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext<Self>) -> Self {
|
||||
let filter = cx.try_global::<CommandPaletteFilter>();
|
||||
|
||||
let commands = cx
|
||||
.available_actions()
|
||||
.into_iter()
|
||||
.filter_map(|action| {
|
||||
let name = action.name();
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
|| f.hidden_action_types.contains(&action.type_id())
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(&name),
|
||||
action,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let delegate =
|
||||
CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
|
||||
|
||||
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||
|
||||
impl FocusableView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommandPalette {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CommandPaletteInterceptor =
|
||||
Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
|
||||
|
||||
pub struct CommandInterceptResult {
|
||||
pub action: Box<dyn Action>,
|
||||
pub string: String,
|
||||
pub positions: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct CommandPaletteDelegate {
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
all_commands: Vec<Command>,
|
||||
commands: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
previous_focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
struct Command {
|
||||
name: String,
|
||||
action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl Clone for Command {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
action: self.action.boxed_clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hit count for each command in the palette.
|
||||
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
|
||||
/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
|
||||
#[derive(Default)]
|
||||
struct HitCounts(HashMap<String, usize>);
|
||||
|
||||
impl CommandPaletteDelegate {
|
||||
fn new(
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
commands: Vec<Command>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
command_palette,
|
||||
all_commands: commands.clone(),
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
previous_focus_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Execute a command...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_ix
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let mut commands = self.all_commands.clone();
|
||||
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
cx.read_global::<HitCounts, _>(|hit_counts, _| {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
Reverse(hit_counts.0.get(&action.name).cloned()),
|
||||
action.name.clone(),
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let mut intercept_result = cx
|
||||
.try_read_global(|interceptor: &CommandPaletteInterceptor, cx| {
|
||||
(interceptor)(&query, cx)
|
||||
})
|
||||
.flatten();
|
||||
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
action: OpenZedURL { url: query.clone() }.boxed_clone(),
|
||||
string: query.clone(),
|
||||
positions: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
positions,
|
||||
}) = intercept_result
|
||||
{
|
||||
if let Some(idx) = matches
|
||||
.iter()
|
||||
.position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
|
||||
{
|
||||
matches.remove(idx);
|
||||
}
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
StringMatch {
|
||||
candidate_id: commands.len() - 1,
|
||||
string,
|
||||
positions,
|
||||
score: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.commands = commands;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_ix = 0;
|
||||
} else {
|
||||
delegate.selected_ix =
|
||||
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(cx);
|
||||
return;
|
||||
}
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
cx.update_global(|hit_counts: &mut HitCounts, _| {
|
||||
*hit_counts.0.entry(command.name).or_default() += 1;
|
||||
});
|
||||
let action = command.action;
|
||||
cx.focus(&self.previous_focus_handle);
|
||||
cx.window_context()
|
||||
.spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) })
|
||||
.detach_and_log_err(cx);
|
||||
self.dismissed(cx);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
&self.previous_focus_handle,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn humanize_action_name(name: &str) -> String {
|
||||
let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
|
||||
let mut result = String::with_capacity(capacity);
|
||||
for char in name.chars() {
|
||||
if char == ':' {
|
||||
if result.ends_with(':') {
|
||||
result.push(' ');
|
||||
} else {
|
||||
result.push(':');
|
||||
}
|
||||
} else if char == '_' {
|
||||
result.push(' ');
|
||||
} else if char.is_uppercase() {
|
||||
if !result.ends_with(' ') {
|
||||
result.push(' ');
|
||||
}
|
||||
result.extend(char.to_lowercase());
|
||||
} else {
|
||||
result.push(char);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command")
|
||||
.field("name", &self.name)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use go_to_line::GoToLine;
|
||||
use gpui::TestAppContext;
|
||||
use language::Point;
|
||||
use project::Project;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
#[test]
|
||||
fn test_humanize_action_name() {
|
||||
assert_eq!(
|
||||
humanize_action_name("editor::GoToDefinition"),
|
||||
"editor: go to definition"
|
||||
);
|
||||
assert_eq!(
|
||||
humanize_action_name("editor::Backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
humanize_action_name("go_to_line::Deploy"),
|
||||
"go to line: deploy"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_text("abc", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(editor.clone()), cx);
|
||||
editor.update(cx, |editor, cx| editor.focus(cx))
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
});
|
||||
|
||||
palette.update(cx, |palette, _| {
|
||||
assert!(palette.delegate.commands.len() > 5);
|
||||
let is_sorted =
|
||||
|actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
|
||||
assert!(is_sorted(&palette.delegate.commands));
|
||||
});
|
||||
|
||||
cx.simulate_input("bcksp");
|
||||
|
||||
palette.update(cx, |palette, _| {
|
||||
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
|
||||
assert_eq!(editor.read(cx).text(cx), "ab")
|
||||
});
|
||||
|
||||
// Add namespace filter, and redeploy the palette
|
||||
cx.update(|cx| {
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_input("bcksp");
|
||||
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
});
|
||||
palette.update(cx, |palette, _| {
|
||||
assert!(palette.delegate.matches.is_empty())
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_line(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-n");
|
||||
|
||||
let editor = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
});
|
||||
editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_input("go to line: Toggle");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.active_modal::<GoToLine>(cx).is_some())
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("3 enter");
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(editor.focus_handle(cx).is_focused(cx));
|
||||
assert_eq!(
|
||||
editor.selections.last::<Point>(cx).range().start,
|
||||
Point::new(2, 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
menu::init();
|
||||
go_to_line::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
Project::init_settings(cx);
|
||||
settings::load_default_keymap(cx);
|
||||
app_state
|
||||
})
|
||||
}
|
||||
}
|
@ -10,15 +10,16 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
@ -30,13 +31,13 @@ smallvec.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
|
||||
serde_json.workspace = true
|
||||
unindent.workspace = true
|
||||
|
@ -2,19 +2,21 @@ pub mod items;
|
||||
mod project_diagnostics_settings;
|
||||
mod toolbar_controls;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
diagnostic_block_renderer,
|
||||
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
|
||||
highlight_diagnostic_message,
|
||||
scroll::autoscroll::Autoscroll,
|
||||
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
|
||||
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
|
||||
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
|
||||
SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
|
||||
@ -23,23 +25,22 @@ use language::{
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
use project_diagnostics_settings::ProjectDiagnosticsSettings;
|
||||
use serde_json::json;
|
||||
use smallvec::SmallVec;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, prelude::*, Icon, IconElement, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
@ -47,20 +48,18 @@ actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
settings::register::<ProjectDiagnosticsSettings>(cx);
|
||||
cx.add_action(ProjectDiagnosticsEditor::deploy);
|
||||
cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
|
||||
items::init(cx);
|
||||
ProjectDiagnosticsSettings::register(cx);
|
||||
cx.observe_new_views(ProjectDiagnosticsEditor::register)
|
||||
.detach();
|
||||
}
|
||||
|
||||
type Event = editor::Event;
|
||||
|
||||
struct ProjectDiagnosticsEditor {
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
editor: ViewHandle<Editor>,
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
editor: View<Editor>,
|
||||
summary: DiagnosticSummary,
|
||||
excerpts: ModelHandle<MultiBuffer>,
|
||||
excerpts: Model<MultiBuffer>,
|
||||
path_states: Vec<PathState>,
|
||||
paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
|
||||
current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
|
||||
@ -89,71 +88,38 @@ struct DiagnosticGroupState {
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
impl Entity for ProjectDiagnosticsEditor {
|
||||
type Event = Event;
|
||||
}
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl View for ProjectDiagnosticsEditor {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectDiagnosticsEditor"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &theme::current(cx).project_diagnostics;
|
||||
PaneBackdrop::new(
|
||||
cx.view_id(),
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.into_any(),
|
||||
)
|
||||
.into_any()
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
let child = if self.path_states.is_empty() {
|
||||
div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.child(Label::new("No problems in workspace"))
|
||||
} else {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
}
|
||||
}
|
||||
div().size_full().child(self.editor.clone())
|
||||
};
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() && !self.path_states.is_empty() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
|
||||
let project = self.project.read(cx);
|
||||
json!({
|
||||
"project": json!({
|
||||
"language_servers": project.language_server_statuses().collect::<Vec<_>>(),
|
||||
"summary": project.diagnostic_summary(false, cx),
|
||||
}),
|
||||
"summary": self.summary,
|
||||
"paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
|
||||
(server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
|
||||
).collect::<HashMap<_, _>>(),
|
||||
"current_diagnostics": self.current_diagnostics.iter().map(|(server_id, paths)|
|
||||
(server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
|
||||
).collect::<HashMap<_, _>>(),
|
||||
"paths_states": self.path_states.iter().map(|state|
|
||||
json!({
|
||||
"path": state.path.path.to_string_lossy(),
|
||||
"groups": state.diagnostic_groups.iter().map(|group|
|
||||
json!({
|
||||
"block_count": group.blocks.len(),
|
||||
"excerpt_count": group.excerpts.len(),
|
||||
})
|
||||
).collect::<Vec<_>>(),
|
||||
})
|
||||
).collect::<Vec<_>>(),
|
||||
})
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::toggle_warnings))
|
||||
.child(child)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectDiagnosticsEditor {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(Self::deploy);
|
||||
}
|
||||
|
||||
fn new(
|
||||
project_handle: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
project_handle: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let project_event_subscription =
|
||||
@ -180,17 +146,23 @@ impl ProjectDiagnosticsEditor {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
|
||||
let editor = cx.add_view(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let focus_in_subscription =
|
||||
cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
|
||||
|
||||
let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
let editor_event_subscription = cx.subscribe(&editor, |this, _, event, cx| {
|
||||
let editor_event_subscription =
|
||||
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
|
||||
cx.emit(event.clone());
|
||||
if event == &editor::Event::Focused && this.path_states.is_empty() {
|
||||
cx.focus_self()
|
||||
if event == &EditorEvent::Focused && this.path_states.is_empty() {
|
||||
cx.focus(&this.focus_handle);
|
||||
}
|
||||
});
|
||||
|
||||
@ -201,12 +173,17 @@ impl ProjectDiagnosticsEditor {
|
||||
summary,
|
||||
workspace,
|
||||
excerpts,
|
||||
focus_handle,
|
||||
editor,
|
||||
path_states: Default::default(),
|
||||
paths_to_update: HashMap::default(),
|
||||
include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
|
||||
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
|
||||
current_diagnostics: HashMap::default(),
|
||||
_subscriptions: vec![project_event_subscription, editor_event_subscription],
|
||||
_subscriptions: vec![
|
||||
project_event_subscription,
|
||||
editor_event_subscription,
|
||||
focus_in_subscription,
|
||||
],
|
||||
};
|
||||
this.update_excerpts(None, cx);
|
||||
this
|
||||
@ -216,8 +193,8 @@ impl ProjectDiagnosticsEditor {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
} else {
|
||||
let workspace_handle = cx.weak_handle();
|
||||
let diagnostics = cx.add_view(|cx| {
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
let diagnostics = cx.new_view(|cx| {
|
||||
ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
|
||||
});
|
||||
workspace.add_item(Box::new(diagnostics), cx);
|
||||
@ -231,6 +208,12 @@ impl ProjectDiagnosticsEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
|
||||
self.editor.focus_handle(cx).focus(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_excerpts(
|
||||
&mut self,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
@ -304,9 +287,10 @@ impl ProjectDiagnosticsEditor {
|
||||
let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
|
||||
let mut cx = cx.clone();
|
||||
let project = project.clone();
|
||||
let this = this.clone();
|
||||
async move {
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
|
||||
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
|
||||
.await
|
||||
.with_context(|| format!("opening buffer for path {path:?}"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@ -321,7 +305,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(Event::TitleChanged);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@ -334,7 +318,7 @@ impl ProjectDiagnosticsEditor {
|
||||
&mut self,
|
||||
path: ProjectPath,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
buffer: Model<Buffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let was_empty = self.path_states.is_empty();
|
||||
@ -618,41 +602,32 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
|
||||
if self.path_states.is_empty() {
|
||||
if self.editor.is_focused(cx) {
|
||||
cx.focus_self();
|
||||
if self.editor.focus_handle(cx).is_focused(cx) {
|
||||
cx.focus(&self.focus_handle);
|
||||
}
|
||||
} else if cx.handle().is_focused(cx) {
|
||||
cx.focus(&self.editor);
|
||||
} else if self.focus_handle.is_focused(cx) {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ProjectDiagnosticsEditor {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
fn tab_content<T: 'static>(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> AnyElement<T> {
|
||||
render_summary(
|
||||
&self.summary,
|
||||
&style.label.text,
|
||||
&theme::current(cx).project_diagnostics,
|
||||
)
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
@ -660,10 +635,82 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some("Project Diagnostics".into())
|
||||
}
|
||||
|
||||
fn tab_content(&self, _detail: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
let label = Label::new("No problems");
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when(self.summary.error_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(self.summary.warning_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
|
||||
) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new_view(|cx| {
|
||||
ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.excerpts.read(cx).is_dirty(cx)
|
||||
}
|
||||
@ -676,207 +723,131 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
true
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
self.editor.save(project, cx)
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: Model<Project>,
|
||||
_: PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(ProjectDiagnosticsEditor::new(
|
||||
self.project.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
))
|
||||
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
self.editor.reload(project, cx)
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a ViewHandle<Self>,
|
||||
self_handle: &'a View<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<&AnyViewHandle> {
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle)
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(&self.editor)
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("diagnostics")
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("diagnostics")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
|
||||
) -> Task<Result<View<Self>>> {
|
||||
Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
|
||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
|
||||
let message: SharedString = message.into();
|
||||
Arc::new(move |cx| {
|
||||
let settings = settings::get::<ThemeSettings>(cx);
|
||||
let theme = &settings.theme.editor;
|
||||
let style = theme.diagnostic_header.clone();
|
||||
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
|
||||
let icon_width = cx.em_width * style.icon_width_factor;
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color)
|
||||
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
|
||||
h_stack()
|
||||
.id("diagnostic header")
|
||||
.py_2()
|
||||
.pl_10()
|
||||
.pr_5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_3()
|
||||
.map(|stack| {
|
||||
stack.child(
|
||||
svg()
|
||||
.size(cx.text_style().font_size)
|
||||
.flex_none()
|
||||
.map(|icon| {
|
||||
if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
icon.path(Icon::XCircle.path())
|
||||
.text_color(Color::Error.color(cx))
|
||||
} else {
|
||||
Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color)
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
icon.constrained()
|
||||
.with_width(icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(cx.gutter_padding),
|
||||
icon.path(Icon::ExclamationTriangle.path())
|
||||
.text_color(Color::Warning.color(cx))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.with_children(diagnostic.source.as_ref().map(|source| {
|
||||
Label::new(
|
||||
format!("{source}: "),
|
||||
style.source.label.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.message.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.clone(),
|
||||
style.message.label.clone().with_font_size(font_size),
|
||||
)
|
||||
.with_highlights(highlights.clone())
|
||||
.contained()
|
||||
.with_style(style.message.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(diagnostic.code.clone().map(|code| {
|
||||
Label::new(code, style.code.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.code.container)
|
||||
.aligned()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(cx.gutter_padding)
|
||||
.with_padding_right(cx.gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("diagnostic header")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary<T: 'static>(
|
||||
summary: &DiagnosticSummary,
|
||||
text_style: &TextStyle,
|
||||
theme: &theme::ProjectDiagnostics,
|
||||
) -> AnyElement<T> {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
Label::new("No problems", text_style.clone()).into_any()
|
||||
} else {
|
||||
let icon_width = theme.tab_icon_width;
|
||||
let icon_spacing = theme.tab_icon_spacing;
|
||||
let summary_spacing = theme.tab_summary_spacing;
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(text_style.color)
|
||||
.constrained()
|
||||
.with_width(icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(icon_spacing),
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
StyledText::new(message.clone()).with_highlights(
|
||||
&cx.text_style(),
|
||||
code_ranges
|
||||
.iter()
|
||||
.map(|range| (range.clone(), highlight_style)),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
summary.error_count.to_string(),
|
||||
LabelStyle {
|
||||
text: text_style.clone(),
|
||||
highlight_text: None,
|
||||
},
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(
|
||||
div()
|
||||
.child(SharedString::from(format!("({code})")))
|
||||
.text_color(cx.theme().colors().text_muted),
|
||||
)
|
||||
.aligned(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Svg::new("icons/warning.svg")
|
||||
.with_color(text_style.color)
|
||||
.constrained()
|
||||
.with_width(icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(summary_spacing)
|
||||
.with_margin_right(icon_spacing),
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(
|
||||
div()
|
||||
.child(SharedString::from(source.clone()))
|
||||
.text_color(cx.theme().colors().text_muted),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
summary.warning_count.to_string(),
|
||||
LabelStyle {
|
||||
text: text_style.clone(),
|
||||
highlight_text: None,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
|
||||
@ -904,7 +875,7 @@ mod tests {
|
||||
display_map::{BlockContext, TransformBlock},
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::{TestAppContext, WindowContext};
|
||||
use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
@ -915,7 +886,7 @@ mod tests {
|
||||
async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
@ -945,7 +916,8 @@ mod tests {
|
||||
let language_server_id = LanguageServerId(0);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let workspace = window.root(cx);
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
@ -1032,7 +1004,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Open the project diagnostics view while there are already diagnostics.
|
||||
let view = window.add_view(cx, |cx| {
|
||||
let view = window.build_view(cx, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
@ -1320,7 +1292,7 @@ mod tests {
|
||||
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
@ -1339,9 +1311,10 @@ mod tests {
|
||||
let server_id_2 = LanguageServerId(101);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let workspace = window.root(cx);
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let view = window.add_view(cx, |cx| {
|
||||
let view = window.build_view(cx, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
@ -1376,7 +1349,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are shown.
|
||||
cx.foreground().run_until_parked();
|
||||
cx.executor().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
@ -1424,7 +1397,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Both language server's diagnostics are shown.
|
||||
cx.foreground().run_until_parked();
|
||||
cx.executor().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
@ -1492,7 +1465,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are updated.
|
||||
cx.foreground().run_until_parked();
|
||||
cx.executor().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
@ -1550,7 +1523,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Both language servers' diagnostics are updated.
|
||||
cx.foreground().run_until_parked();
|
||||
cx.executor().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
@ -1586,8 +1559,9 @@ mod tests {
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
@ -1596,7 +1570,7 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
|
||||
fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
snapshot
|
||||
@ -1607,23 +1581,25 @@ mod tests {
|
||||
TransformBlock::Custom(block) => block
|
||||
.render(&mut BlockContext {
|
||||
view_context: cx,
|
||||
anchor_x: 0.,
|
||||
scroll_x: 0.,
|
||||
gutter_padding: 0.,
|
||||
gutter_width: 0.,
|
||||
line_height: 0.,
|
||||
em_width: 0.,
|
||||
anchor_x: px(0.),
|
||||
gutter_padding: px(0.),
|
||||
gutter_width: px(0.),
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
})
|
||||
.name()?
|
||||
.to_string(),
|
||||
.inner_id()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
|
||||
TransformBlock::ExcerptHeader {
|
||||
starts_new_buffer, ..
|
||||
} => {
|
||||
if *starts_new_buffer {
|
||||
"path header block".to_string()
|
||||
"path header block".into()
|
||||
} else {
|
||||
"collapsed context".to_string()
|
||||
"collapsed context".into()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,27 +1,105 @@
|
||||
use collections::HashSet;
|
||||
use editor::{Editor, GoToDiagnostic};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use crate::{Deploy, ProjectDiagnosticsEditor};
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
active_editor: Option<WeakViewHandle<Editor>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(DiagnosticIndicator::go_to_next_diagnostic);
|
||||
impl Render for DiagnosticIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_stack().child(
|
||||
IconElement::new(Icon::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
),
|
||||
(0, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
(error_count, 0) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(Label::new(error_count.to_string()).size(LabelSize::Small)),
|
||||
(error_count, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
};
|
||||
|
||||
let status = if !self.in_progress_checks.is_empty() {
|
||||
Some(
|
||||
Label::new("Checking…")
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message = diagnostic.message.split('\n').next().unwrap().to_string();
|
||||
Some(
|
||||
Button::new("diagnostic_message", message)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.go_to_next_diagnostic(cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.h(rems(1.375))
|
||||
.gap_2()
|
||||
.child(
|
||||
ButtonLike::new("diagnostic-indicator")
|
||||
.child(diagnostic_indicator)
|
||||
.tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})),
|
||||
)
|
||||
.children(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticIndicator {
|
||||
@ -32,19 +110,23 @@ impl DiagnosticIndicator {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
@ -58,15 +140,15 @@ impl DiagnosticIndicator {
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) {
|
||||
fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let cursor_position = editor.selections.newest::<usize>(cx).head();
|
||||
@ -83,146 +165,7 @@ impl DiagnosticIndicator {
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for DiagnosticIndicator {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for DiagnosticIndicator {
|
||||
fn ui_name() -> &'static str {
|
||||
"DiagnosticIndicator"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Summary {}
|
||||
enum Message {}
|
||||
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let in_progress = !self.in_progress_checks.is_empty();
|
||||
let mut element = Flex::row().with_child(
|
||||
MouseEventHandler::new::<Summary, _>(0, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.diagnostic_summary
|
||||
.style_for(state);
|
||||
|
||||
let mut summary_row = Flex::row();
|
||||
if self.summary.error_count > 0 {
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(style.icon_color_error)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(style.icon_spacing),
|
||||
);
|
||||
summary_row.add_child(
|
||||
Label::new(self.summary.error_count.to_string(), style.text.clone())
|
||||
.aligned(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.summary.warning_count > 0 {
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/warning.svg")
|
||||
.with_color(style.icon_color_warning)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(style.icon_spacing)
|
||||
.with_margin_left(if self.summary.error_count > 0 {
|
||||
style.summary_spacing
|
||||
} else {
|
||||
0.
|
||||
}),
|
||||
);
|
||||
summary_row.add_child(
|
||||
Label::new(self.summary.warning_count.to_string(), style.text.clone())
|
||||
.aligned(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/check_circle.svg")
|
||||
.with_color(style.icon_color_ok)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.into_any_named("ok-icon"),
|
||||
);
|
||||
}
|
||||
|
||||
summary_row
|
||||
.constrained()
|
||||
.with_height(style.height)
|
||||
.contained()
|
||||
.with_style(if self.summary.error_count > 0 {
|
||||
style.container_error
|
||||
} else if self.summary.warning_count > 0 {
|
||||
style.container_warning
|
||||
} else {
|
||||
style.container_ok
|
||||
})
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Summary>(
|
||||
0,
|
||||
"Project Diagnostics",
|
||||
Some(Box::new(crate::Deploy)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any(),
|
||||
);
|
||||
|
||||
let style = &theme::current(cx).workspace.status_bar;
|
||||
let item_spacing = style.item_spacing;
|
||||
|
||||
if in_progress {
|
||||
element.add_child(
|
||||
Label::new("Checking…", style.diagnostic_message.default.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing),
|
||||
);
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message_style = style.diagnostic_message.clone();
|
||||
element.add_child(
|
||||
MouseEventHandler::new::<Message, _>(1, cx, |state, _| {
|
||||
Label::new(
|
||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
message_style.style_for(state).text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.go_to_next_diagnostic(&Default::default(), cx)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
element.into_any_named("diagnostic indicator")
|
||||
}
|
||||
|
||||
fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
|
||||
serde_json::json!({ "summary": self.summary })
|
||||
}
|
||||
}
|
||||
impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
|
||||
|
||||
impl StatusItemView for DiagnosticIndicator {
|
||||
fn set_active_pane_item(
|
||||
|
@ -11,14 +11,14 @@ pub struct ProjectDiagnosticsSettingsContent {
|
||||
include_warnings: Option<bool>,
|
||||
}
|
||||
|
||||
impl settings::Setting for ProjectDiagnosticsSettings {
|
||||
impl settings::Settings for ProjectDiagnosticsSettings {
|
||||
const KEY: Option<&'static str> = Some("diagnostics");
|
||||
type FileContent = ProjectDiagnosticsSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_cx: &gpui::AppContext,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
|
@ -1,55 +1,44 @@
|
||||
use crate::{ProjectDiagnosticsEditor, ToggleWarnings};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, Entity, EventContext, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct ToolbarControls {
|
||||
editor: Option<WeakViewHandle<ProjectDiagnosticsEditor>>,
|
||||
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
|
||||
}
|
||||
|
||||
impl Entity for ToolbarControls {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ToolbarControls {
|
||||
fn ui_name() -> &'static str {
|
||||
"ToolbarControls"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
impl Render for ToolbarControls {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let include_warnings = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade(cx))
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| editor.read(cx).include_warnings)
|
||||
.unwrap_or(false);
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
"Exclude Warnings".into()
|
||||
"Exclude Warnings"
|
||||
} else {
|
||||
"Include Warnings".into()
|
||||
"Include Warnings"
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(render_toggle_button(
|
||||
0,
|
||||
"icons/warning.svg",
|
||||
include_warnings,
|
||||
(tooltip, Some(Box::new(ToggleWarnings))),
|
||||
cx,
|
||||
move |this, cx| {
|
||||
if let Some(editor) = this.editor.and_then(|editor| editor.upgrade(cx)) {
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx)
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any()
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
|
||||
|
||||
impl ToolbarItemView for ToolbarControls {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
@ -59,7 +48,7 @@ impl ToolbarItemView for ToolbarControls {
|
||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
||||
self.editor = Some(editor.downgrade());
|
||||
ToolbarItemLocation::PrimaryRight { flex: None }
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
@ -74,42 +63,3 @@ impl ToolbarControls {
|
||||
ToolbarControls { editor: None }
|
||||
}
|
||||
}
|
||||
|
||||
fn render_toggle_button<
|
||||
F: 'static + Fn(&mut ToolbarControls, &mut EventContext<ToolbarControls>),
|
||||
>(
|
||||
index: usize,
|
||||
icon: &'static str,
|
||||
toggled: bool,
|
||||
tooltip: (String, Option<Box<dyn Action>>),
|
||||
cx: &mut ViewContext<ToolbarControls>,
|
||||
on_click: F,
|
||||
) -> AnyElement<ToolbarControls> {
|
||||
enum Button {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
let (tooltip_text, action) = tooltip;
|
||||
|
||||
MouseEventHandler::new::<Button, _>(index, cx, |mouse_state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.toolbar
|
||||
.toggleable_tool
|
||||
.in_state(toggled)
|
||||
.style_for(mouse_state);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| on_click(view, cx))
|
||||
.with_tooltip::<Button>(index, tooltip_text, action, theme.tooltip.clone(), cx)
|
||||
.into_any_named("quick action bar button")
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
[package]
|
||||
name = "diagnostics2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/diagnostics.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
|
||||
serde_json.workspace = true
|
||||
unindent.workspace = true
|
File diff suppressed because it is too large
Load Diff
@ -1,187 +0,0 @@
|
||||
use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
use crate::{Deploy, ProjectDiagnosticsEditor};
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Render for DiagnosticIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_stack().child(
|
||||
IconElement::new(Icon::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
),
|
||||
(0, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
(error_count, 0) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(Label::new(error_count.to_string()).size(LabelSize::Small)),
|
||||
(error_count, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
};
|
||||
|
||||
let status = if !self.in_progress_checks.is_empty() {
|
||||
Some(
|
||||
Label::new("Checking…")
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message = diagnostic.message.split('\n').next().unwrap().to_string();
|
||||
Some(
|
||||
Button::new("diagnostic_message", message)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.go_to_next_diagnostic(cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.h(rems(1.375))
|
||||
.gap_2()
|
||||
.child(
|
||||
ButtonLike::new("diagnostic-indicator")
|
||||
.child(diagnostic_indicator)
|
||||
.tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})),
|
||||
)
|
||||
.children(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticIndicator {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect(),
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_diagnostic: None,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let cursor_position = editor.selections.newest::<usize>(cx).head();
|
||||
let new_diagnostic = buffer
|
||||
.snapshot(cx)
|
||||
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
|
||||
.filter(|entry| !entry.range.is_empty())
|
||||
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
|
||||
.map(|entry| entry.diagnostic);
|
||||
if new_diagnostic != self.current_diagnostic {
|
||||
self.current_diagnostic = new_diagnostic;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
|
||||
|
||||
impl StatusItemView for DiagnosticIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
self.active_editor = Some(editor.downgrade());
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update));
|
||||
self.update(editor, cx);
|
||||
} else {
|
||||
self.active_editor = None;
|
||||
self.current_diagnostic = None;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ProjectDiagnosticsSettings {
|
||||
pub include_warnings: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ProjectDiagnosticsSettingsContent {
|
||||
include_warnings: Option<bool>,
|
||||
}
|
||||
|
||||
impl settings::Settings for ProjectDiagnosticsSettings {
|
||||
const KEY: Option<&'static str> = Some("diagnostics");
|
||||
type FileContent = ProjectDiagnosticsSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct ToolbarControls {
|
||||
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
|
||||
}
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let include_warnings = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| editor.read(cx).include_warnings)
|
||||
.unwrap_or(false);
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
"Exclude Warnings"
|
||||
} else {
|
||||
"Include Warnings"
|
||||
};
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
|
||||
|
||||
impl ToolbarItemView for ToolbarControls {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
||||
self.editor = Some(editor.downgrade());
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarControls {
|
||||
pub fn new() -> Self {
|
||||
ToolbarControls { editor: None }
|
||||
}
|
||||
}
|
@ -9,26 +9,28 @@ path = "src/file_finder.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
text = { path = "../text" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
|
||||
serde_json.workspace = true
|
||||
ctor.workspace = true
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,37 +0,0 @@
|
||||
[package]
|
||||
name = "file_finder2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/file_finder.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
util = { path = "../util" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
|
||||
serde_json.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
File diff suppressed because it is too large
Load Diff
@ -9,15 +9,17 @@ path = "src/go_to_line.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
settings = { path = "../settings" }
|
||||
text = { path = "../text" }
|
||||
workspace = { path = "../workspace" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
serde.workspace = true
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
postage.workspace = true
|
||||
theme = { path = "../theme" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
|
@ -1,104 +1,107 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
|
||||
View, ViewContext, ViewHandle,
|
||||
actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
||||
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||
};
|
||||
use menu::{Cancel, Confirm};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, prelude::*, v_stack, Label};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::{Modal, Workspace};
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(go_to_line, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(GoToLine::toggle);
|
||||
cx.add_action(GoToLine::confirm);
|
||||
cx.add_action(GoToLine::cancel);
|
||||
cx.observe_new_views(GoToLine::register).detach();
|
||||
}
|
||||
|
||||
pub struct GoToLine {
|
||||
line_editor: ViewHandle<Editor>,
|
||||
active_editor: ViewHandle<Editor>,
|
||||
prev_scroll_position: Option<Vector2F>,
|
||||
cursor_point: Point,
|
||||
max_point: Point,
|
||||
has_focus: bool,
|
||||
line_editor: View<Editor>,
|
||||
active_editor: View<Editor>,
|
||||
current_text: SharedString,
|
||||
prev_scroll_position: Option<gpui::Point<f32>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
impl ModalView for GoToLine {}
|
||||
|
||||
impl FocusableView for GoToLine {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.line_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for GoToLine {}
|
||||
|
||||
impl GoToLine {
|
||||
pub fn new(active_editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let line_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
Some(Arc::new(|theme| theme.picker.input_editor.clone())),
|
||||
cx,
|
||||
)
|
||||
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
let handle = cx.view().downgrade();
|
||||
editor.register_action(move |_: &Toggle, cx| {
|
||||
let Some(editor) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = editor.read(cx).workspace() else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
|
||||
})
|
||||
});
|
||||
cx.subscribe(&line_editor, Self::on_line_editor_event)
|
||||
.detach();
|
||||
}
|
||||
|
||||
let (scroll_position, cursor_point, max_point) = active_editor.update(cx, |editor, cx| {
|
||||
let scroll_position = editor.scroll_position(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
(
|
||||
Some(scroll_position),
|
||||
editor.selections.newest(cx).head(),
|
||||
buffer.max_point(),
|
||||
)
|
||||
});
|
||||
pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let line_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||
let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
|
||||
|
||||
let editor = active_editor.read(cx);
|
||||
let cursor = editor.selections.last::<Point>(cx).head();
|
||||
let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
|
||||
let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
|
||||
|
||||
let current_text = format!(
|
||||
"line {} of {} (column {})",
|
||||
cursor.row + 1,
|
||||
last_line + 1,
|
||||
cursor.column + 1,
|
||||
);
|
||||
|
||||
Self {
|
||||
line_editor,
|
||||
active_editor,
|
||||
prev_scroll_position: scroll_position,
|
||||
cursor_point,
|
||||
max_point,
|
||||
has_focus: false,
|
||||
current_text: current_text.into(),
|
||||
prev_scroll_position: Some(scroll_position),
|
||||
_subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|active_item| active_item.downcast::<Editor>())
|
||||
{
|
||||
workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| GoToLine::new(editor, cx)));
|
||||
fn release(&mut self, window: AnyWindowHandle, cx: &mut AppContext) {
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
let scroll_position = self.prev_scroll_position.take();
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.highlight_rows(None);
|
||||
if let Some(scroll_position) = scroll_position {
|
||||
editor.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
self.prev_scroll_position.take();
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([point..point])
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(Event::Dismissed);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn on_line_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
_: View<Editor>,
|
||||
event: &editor::EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::BufferEdited { .. } => {
|
||||
editor::EditorEvent::Blurred => cx.emit(DismissEvent),
|
||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
@ -111,9 +114,6 @@ impl GoToLine {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
|
||||
let line_editor = self.line_editor.read(cx).text(cx);
|
||||
@ -128,73 +128,61 @@ impl GoToLine {
|
||||
column.unwrap_or(0).saturating_sub(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
impl Entity for GoToLine {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, cx: &mut AppContext) {
|
||||
let scroll_position = self.prev_scroll_position.take();
|
||||
self.active_editor.window().update(cx, |cx| {
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.highlight_rows(None);
|
||||
if let Some(scroll_position) = scroll_position {
|
||||
editor.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
})
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([point..point])
|
||||
});
|
||||
editor.focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl View for GoToLine {
|
||||
fn ui_name() -> &'static str {
|
||||
"GoToLine"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).picker;
|
||||
|
||||
let label = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines",
|
||||
self.cursor_point.row + 1,
|
||||
self.cursor_point.column + 1,
|
||||
self.max_point.row + 1
|
||||
);
|
||||
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
ChildView::new(&self.line_editor, cx)
|
||||
.contained()
|
||||
.with_style(theme.input_editor.container),
|
||||
impl Render for GoToLine {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.elevation_2(cx)
|
||||
.key_context("GoToLine")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w_96()
|
||||
.child(
|
||||
v_stack()
|
||||
.px_1()
|
||||
.pt_0p5()
|
||||
.gap_px()
|
||||
.child(
|
||||
v_stack()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.child(div().px_1().py_0p5().child(self.line_editor.clone())),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(label, theme.no_matches.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.no_matches.container),
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(Label::new(self.current_text.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.constrained()
|
||||
.with_max_width(500.0)
|
||||
.into_any_named("go to line")
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
cx.focus(&self.line_editor);
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for GoToLine {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Dismissed)
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "go_to_line2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/go_to_line.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
serde.workspace = true
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
postage.workspace = true
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
@ -1,188 +0,0 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
||||
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, prelude::*, v_stack, Label};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(go_to_line, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(GoToLine::register).detach();
|
||||
}
|
||||
|
||||
pub struct GoToLine {
|
||||
line_editor: View<Editor>,
|
||||
active_editor: View<Editor>,
|
||||
current_text: SharedString,
|
||||
prev_scroll_position: Option<gpui::Point<f32>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ModalView for GoToLine {}
|
||||
|
||||
impl FocusableView for GoToLine {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.line_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for GoToLine {}
|
||||
|
||||
impl GoToLine {
|
||||
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
let handle = cx.view().downgrade();
|
||||
editor.register_action(move |_: &Toggle, cx| {
|
||||
let Some(editor) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = editor.read(cx).workspace() else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let line_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||
let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
|
||||
|
||||
let editor = active_editor.read(cx);
|
||||
let cursor = editor.selections.last::<Point>(cx).head();
|
||||
let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
|
||||
let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
|
||||
|
||||
let current_text = format!(
|
||||
"line {} of {} (column {})",
|
||||
cursor.row + 1,
|
||||
last_line + 1,
|
||||
cursor.column + 1,
|
||||
);
|
||||
|
||||
Self {
|
||||
line_editor,
|
||||
active_editor,
|
||||
current_text: current_text.into(),
|
||||
prev_scroll_position: Some(scroll_position),
|
||||
_subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&mut self, window: AnyWindowHandle, cx: &mut AppContext) {
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
let scroll_position = self.prev_scroll_position.take();
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.highlight_rows(None);
|
||||
if let Some(scroll_position) = scroll_position {
|
||||
editor.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn on_line_editor_event(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
event: &editor::EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::EditorEvent::Blurred => cx.emit(DismissEvent),
|
||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
let display_point = point.to_display_point(&snapshot);
|
||||
let row = display_point.row();
|
||||
active_editor.highlight_rows(Some(row..row + 1));
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
|
||||
let line_editor = self.line_editor.read(cx).text(cx);
|
||||
let mut components = line_editor
|
||||
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
|
||||
.map(str::trim)
|
||||
.fuse();
|
||||
let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
|
||||
let column = components.next().and_then(|col| col.parse::<u32>().ok());
|
||||
Some(Point::new(
|
||||
row.saturating_sub(1),
|
||||
column.unwrap_or(0).saturating_sub(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([point..point])
|
||||
});
|
||||
editor.focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GoToLine {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.elevation_2(cx)
|
||||
.key_context("GoToLine")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w_96()
|
||||
.child(
|
||||
v_stack()
|
||||
.px_1()
|
||||
.pt_0p5()
|
||||
.gap_px()
|
||||
.child(
|
||||
v_stack()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.child(div().px_1().py_0p5().child(self.line_editor.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(Label::new(self.current_text.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -26,28 +26,28 @@ serde_json.workspace = true
|
||||
|
||||
collections = { path = "../collections" }
|
||||
command_palette = { path = "../command_palette" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
theme = { path = "../theme" }
|
||||
language_selector = { path = "../language_selector"}
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2"}
|
||||
diagnostics = { path = "../diagnostics" }
|
||||
zed-actions = { path = "../zed-actions" }
|
||||
zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
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"] }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
|
@ -1,6 +1,6 @@
|
||||
use command_palette::CommandInterceptResult;
|
||||
use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive};
|
||||
use gpui::{impl_actions, Action, AppContext};
|
||||
use gpui::{impl_actions, Action, AppContext, ViewContext};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{SaveIntent, Workspace};
|
||||
|
||||
@ -22,8 +22,8 @@ pub struct GoToLine {
|
||||
|
||||
impl_actions!(vim, [GoToLine]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, action: &GoToLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
move_cursor(vim, Motion::StartOfDocument, Some(action.line as usize), cx);
|
||||
@ -293,14 +293,11 @@ mod test {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::test::{NeovimBackedTestContext, VimTestContext};
|
||||
use gpui::{executor::Foreground, TestAppContext};
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_basics(cx: &mut TestAppContext) {
|
||||
if let Foreground::Deterministic { cx_id: _, executor } = cx.foreground().as_ref() {
|
||||
executor.run_until_parked();
|
||||
}
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
@ -410,15 +407,14 @@ mod test {
|
||||
// conflict!
|
||||
cx.simulate_keystrokes(["i", "@", "escape"]);
|
||||
cx.simulate_keystrokes([":", "w", "enter"]);
|
||||
let window = cx.window;
|
||||
assert!(window.has_pending_prompt(cx.cx));
|
||||
assert!(cx.has_pending_prompt());
|
||||
// "Cancel"
|
||||
window.simulate_prompt_answer(0, cx.cx);
|
||||
cx.simulate_prompt_answer(0);
|
||||
assert_eq!(fs.load(&path).await.unwrap(), "oops\n");
|
||||
assert!(!window.has_pending_prompt(cx.cx));
|
||||
assert!(!cx.has_pending_prompt());
|
||||
// force overwrite
|
||||
cx.simulate_keystrokes([":", "w", "!", "enter"]);
|
||||
assert!(!window.has_pending_prompt(cx.cx));
|
||||
assert!(!cx.has_pending_prompt());
|
||||
assert_eq!(fs.load(&path).await.unwrap(), "@@\n");
|
||||
}
|
||||
|
||||
|
@ -1,43 +1,46 @@
|
||||
use crate::{Vim, VimEvent};
|
||||
use editor::{EditorBlurred, EditorFocused, EditorReleased};
|
||||
use gpui::AppContext;
|
||||
use crate::Vim;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use gpui::{AppContext, Entity, EntityId, View, ViewContext, WindowContext};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.subscribe_global(focused).detach();
|
||||
cx.subscribe_global(blurred).detach();
|
||||
cx.subscribe_global(released).detach();
|
||||
cx.observe_new_views(|_, cx: &mut ViewContext<Editor>| {
|
||||
let editor = cx.view().clone();
|
||||
cx.subscribe(&editor, |_, editor, event: &EditorEvent, cx| match event {
|
||||
EditorEvent::Focused => cx.window_context().defer(|cx| focused(editor, cx)),
|
||||
EditorEvent::Blurred => cx.window_context().defer(|cx| blurred(editor, cx)),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let id = cx.view().entity_id();
|
||||
cx.on_release(move |_, _, cx| released(id, cx)).detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
||||
if let Some(previously_active_editor) = Vim::read(cx).active_editor.clone() {
|
||||
previously_active_editor.window().update(cx, |cx| {
|
||||
fn focused(editor: View<Editor>, cx: &mut WindowContext) {
|
||||
if Vim::read(cx).active_editor.clone().is_some() {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |previously_active_editor, cx| {
|
||||
vim.unhook_vim_settings(previously_active_editor, cx)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.set_active_editor(editor.clone(), cx);
|
||||
if vim.enabled {
|
||||
cx.emit_global(VimEvent::ModeChanged {
|
||||
mode: vim.state().mode,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.recorded_actions.clear();
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
if previous_editor
|
||||
.upgrade()
|
||||
.is_some_and(|previous| previous == editor.clone())
|
||||
{
|
||||
vim.clear_operator(cx);
|
||||
vim.active_editor = None;
|
||||
vim.editor_subscription = None;
|
||||
@ -46,20 +49,19 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
|
||||
editor.update(cx, |editor, cx| vim.unhook_vim_settings(editor, cx))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
fn released(entity_id: EntityId, cx: &mut AppContext) {
|
||||
cx.update_global(|vim: &mut Vim, _| {
|
||||
if vim
|
||||
.active_editor
|
||||
.as_ref()
|
||||
.is_some_and(|previous| previous.entity_id() == entity_id)
|
||||
{
|
||||
vim.active_editor = None;
|
||||
vim.editor_subscription = None;
|
||||
}
|
||||
}
|
||||
vim.editor_states.remove(&editor.id())
|
||||
});
|
||||
vim.editor_states.remove(&entity_id)
|
||||
});
|
||||
}
|
||||
|
||||
@ -67,7 +69,7 @@ fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
|
||||
mod test {
|
||||
use crate::{test::VimTestContext, Vim};
|
||||
use editor::Editor;
|
||||
use gpui::View;
|
||||
use gpui::{Context, Entity};
|
||||
use language::Buffer;
|
||||
|
||||
// regression test for blur called with a different active editor
|
||||
@ -75,18 +77,28 @@ mod test {
|
||||
async fn test_blur_focus(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let buffer = cx.add_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
|
||||
let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
|
||||
let window2 = cx.add_window(|cx| Editor::for_buffer(buffer, None, cx));
|
||||
let editor2 = cx.read(|cx| window2.root(cx)).unwrap();
|
||||
let editor2 = cx
|
||||
.update(|cx| {
|
||||
window2.update(cx, |_, cx| {
|
||||
cx.focus_self();
|
||||
cx.view().clone()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let vim = Vim::read(cx);
|
||||
assert_eq!(vim.active_editor.unwrap().id(), editor2.id())
|
||||
assert_eq!(
|
||||
vim.active_editor.as_ref().unwrap().entity_id(),
|
||||
editor2.entity_id(),
|
||||
)
|
||||
});
|
||||
|
||||
// no panic when blurring an editor in a different window.
|
||||
cx.update_editor(|editor1, cx| {
|
||||
editor1.focus_out(cx.handle().into_any(), cx);
|
||||
editor1.handle_blur(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
use crate::{normal::repeat, state::Mode, Vim};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::{actions, Action, AppContext, ViewContext};
|
||||
use gpui::{actions, Action, ViewContext};
|
||||
use language::SelectionGoal;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [NormalBefore]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(normal_before);
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(normal_before);
|
||||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
@ -38,10 +38,6 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::executor::Deterministic;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
@ -60,76 +56,70 @@ mod test {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_counts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
async fn test_insert_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ-hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("h----ˇ-ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("---ˇ-h-----ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("----h-----ello--ˇ-\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_repeat(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
async fn test_insert_with_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("--ˇ-hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ--hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("-----ˇ---hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["1", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
|
||||
}
|
||||
}
|
||||
|
@ -1,58 +1,40 @@
|
||||
use gpui::{
|
||||
elements::{Empty, Label},
|
||||
AnyElement, Element, Entity, Subscription, View, ViewContext,
|
||||
};
|
||||
use gpui::{div, Element, Render, Subscription, ViewContext};
|
||||
use settings::SettingsStore;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
|
||||
|
||||
use crate::{state::Mode, Vim, VimEvent, VimModeSetting};
|
||||
use crate::{state::Mode, Vim};
|
||||
|
||||
pub struct ModeIndicator {
|
||||
pub mode: Option<Mode>,
|
||||
_subscription: Subscription,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ModeIndicator {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let handle = cx.handle().downgrade();
|
||||
let _subscriptions = vec![
|
||||
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)),
|
||||
cx.observe_global::<SettingsStore>(|this, cx| this.update_mode(cx)),
|
||||
];
|
||||
|
||||
let _subscription = cx.subscribe_global::<VimEvent, _>(move |&event, cx| {
|
||||
if let Some(mode_indicator) = handle.upgrade(cx) {
|
||||
match event {
|
||||
VimEvent::ModeChanged { mode } => {
|
||||
mode_indicator.window().update(cx, |cx| {
|
||||
mode_indicator.update(cx, move |mode_indicator, cx| {
|
||||
mode_indicator.set_mode(mode, cx);
|
||||
})
|
||||
});
|
||||
let mut this = Self {
|
||||
mode: None,
|
||||
_subscriptions,
|
||||
};
|
||||
this.update_mode(cx);
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.observe_global::<SettingsStore, _>(move |mode_indicator, cx| {
|
||||
if settings::get::<VimModeSetting>(cx).0 {
|
||||
mode_indicator.mode = cx
|
||||
.has_global::<Vim>()
|
||||
.then(|| cx.global::<Vim>().state().mode);
|
||||
} else {
|
||||
mode_indicator.mode.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
fn update_mode(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// Vim doesn't exist in some tests
|
||||
let mode = cx
|
||||
.has_global::<Vim>()
|
||||
.then(|| {
|
||||
let vim = cx.global::<Vim>();
|
||||
vim.enabled.then(|| vim.state().mode)
|
||||
})
|
||||
.flatten();
|
||||
if !cx.has_global::<Vim>() {
|
||||
return;
|
||||
}
|
||||
|
||||
Self {
|
||||
mode,
|
||||
_subscription,
|
||||
let vim = Vim::read(cx);
|
||||
if vim.enabled {
|
||||
self.mode = Some(vim.state().mode);
|
||||
} else {
|
||||
self.mode = None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,22 +46,12 @@ impl ModeIndicator {
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ModeIndicator {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ModeIndicator {
|
||||
fn ui_name() -> &'static str {
|
||||
"ModeIndicatorView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
impl Render for ModeIndicator {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Some(mode) = self.mode.as_ref() else {
|
||||
return Empty::new().into_any();
|
||||
return div().into_any();
|
||||
};
|
||||
|
||||
let theme = &theme::current(cx).workspace.status_bar;
|
||||
|
||||
let text = match mode {
|
||||
Mode::Normal => "-- NORMAL --",
|
||||
Mode::Insert => "-- INSERT --",
|
||||
@ -87,10 +59,7 @@ impl View for ModeIndicator {
|
||||
Mode::VisualLine => "-- VISUAL LINE --",
|
||||
Mode::VisualBlock => "-- VISUAL BLOCK --",
|
||||
};
|
||||
Label::new(text, theme.vim_mode_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.vim_mode_indicator.container)
|
||||
.into_any()
|
||||
Label::new(text).size(LabelSize::Small).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use editor::{
|
||||
movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
|
||||
Bias, CharKind, DisplayPoint, ToOffset,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
||||
use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
|
||||
use language::{Point, Selection, SelectionGoal};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
@ -105,6 +105,21 @@ struct RepeatFind {
|
||||
backwards: bool,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
vim,
|
||||
[
|
||||
RepeatFind,
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
FirstNonWhitespace,
|
||||
Down,
|
||||
Up,
|
||||
PreviousWordStart,
|
||||
NextWordEnd,
|
||||
NextWordStart
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
@ -123,25 +138,12 @@ actions!(
|
||||
GoToColumn,
|
||||
]
|
||||
);
|
||||
impl_actions!(
|
||||
vim,
|
||||
[
|
||||
NextWordStart,
|
||||
NextWordEnd,
|
||||
PreviousWordStart,
|
||||
RepeatFind,
|
||||
Up,
|
||||
Down,
|
||||
FirstNonWhitespace,
|
||||
EndOfLine,
|
||||
StartOfLine,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
|
||||
cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||
workspace
|
||||
.register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
|
||||
workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
|
||||
motion(
|
||||
Motion::Down {
|
||||
display_lines: action.display_lines,
|
||||
@ -149,7 +151,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
|
||||
motion(
|
||||
Motion::Up {
|
||||
display_lines: action.display_lines,
|
||||
@ -157,8 +159,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
||||
cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
||||
workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
|
||||
motion(
|
||||
Motion::FirstNonWhitespace {
|
||||
display_lines: action.display_lines,
|
||||
@ -166,7 +168,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
|
||||
motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: action.display_lines,
|
||||
@ -174,7 +176,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
|
||||
motion(
|
||||
Motion::EndOfLine {
|
||||
display_lines: action.display_lines,
|
||||
@ -182,45 +184,53 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
|
||||
motion(Motion::CurrentLine, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
|
||||
motion(Motion::StartOfParagraph, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
|
||||
motion(Motion::EndOfParagraph, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
|
||||
motion(Motion::StartOfDocument, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
|
||||
workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
|
||||
motion(Motion::EndOfDocument, cx)
|
||||
});
|
||||
workspace
|
||||
.register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
|
||||
|
||||
cx.add_action(
|
||||
workspace.register_action(
|
||||
|_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
|
||||
motion(Motion::NextWordStart { ignore_punctuation }, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
workspace.register_action(
|
||||
|_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
|
||||
motion(Motion::NextWordEnd { ignore_punctuation }, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
workspace.register_action(
|
||||
|_: &mut Workspace,
|
||||
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
|
||||
cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
|
||||
motion(Motion::NextLineStart, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
|
||||
motion(Motion::StartOfLineDownward, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
|
||||
workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
|
||||
motion(Motion::EndOfLineDownward, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
|
||||
cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
|
||||
workspace
|
||||
.register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
|
||||
workspace.register_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
|
||||
repeat_motion(action.backwards, cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
||||
@ -578,9 +588,9 @@ fn up_down_buffer_rows(
|
||||
SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
|
||||
SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
|
||||
_ => {
|
||||
let x = map.x_for_point(point, text_layout_details);
|
||||
goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
|
||||
(select_nth_wrapped_row, x)
|
||||
let x = map.x_for_display_point(point, text_layout_details);
|
||||
goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
|
||||
(select_nth_wrapped_row, x.0)
|
||||
}
|
||||
};
|
||||
|
||||
@ -608,7 +618,7 @@ fn up_down_buffer_rows(
|
||||
}
|
||||
|
||||
let new_col = if i == goal_wrap {
|
||||
map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
|
||||
map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
|
||||
} else {
|
||||
map.line_len(begin_folded_line.row())
|
||||
};
|
||||
@ -943,7 +953,6 @@ pub(crate) fn next_line_end(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod test {
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
@ -20,7 +20,7 @@ use crate::{
|
||||
use collections::HashSet;
|
||||
use editor::scroll::autoscroll::Autoscroll;
|
||||
use editor::{Bias, DisplayPoint};
|
||||
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
||||
use gpui::{actions, ViewContext, WindowContext};
|
||||
use language::SelectionGoal;
|
||||
use log::error;
|
||||
use workspace::Workspace;
|
||||
@ -52,38 +52,31 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
paste::init(cx);
|
||||
repeat::init(cx);
|
||||
scroll::init(cx);
|
||||
search::init(cx);
|
||||
substitute::init(cx);
|
||||
increment::init(cx);
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(insert_after);
|
||||
workspace.register_action(insert_before);
|
||||
workspace.register_action(insert_first_non_whitespace);
|
||||
workspace.register_action(insert_end_of_line);
|
||||
workspace.register_action(insert_line_above);
|
||||
workspace.register_action(insert_line_below);
|
||||
workspace.register_action(change_case);
|
||||
workspace.register_action(yank_line);
|
||||
|
||||
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);
|
||||
cx.add_action(yank_line);
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(vim, Motion::Left, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(vim, Motion::Right, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let times = vim.take_count(cx);
|
||||
@ -97,7 +90,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.take_count(cx);
|
||||
@ -111,7 +104,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let mut times = vim.take_count(cx).unwrap_or(1);
|
||||
@ -129,8 +122,15 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
paste::register(workspace, cx);
|
||||
repeat::register(workspace, cx);
|
||||
scroll::register(workspace, cx);
|
||||
search::register(workspace, cx);
|
||||
substitute::register(workspace, cx);
|
||||
increment::register(workspace, cx);
|
||||
}
|
||||
|
||||
pub fn normal_motion(
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use editor::{scroll::autoscroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use gpui::{impl_actions, AppContext, WindowContext};
|
||||
use gpui::{impl_actions, ViewContext, WindowContext};
|
||||
use language::{Bias, Point};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
@ -24,8 +24,8 @@ struct Decrement {
|
||||
|
||||
impl_actions!(vim, [Increment, Decrement]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, action: &Increment, cx| {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, action: &Increment, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
@ -33,7 +33,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
increment(vim, count as i32, step, cx)
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &Decrement, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, action: &Decrement, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
|
@ -4,7 +4,7 @@ use editor::{
|
||||
display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::{impl_actions, AppContext, ViewContext};
|
||||
use gpui::{impl_actions, ViewContext};
|
||||
use language::{Bias, SelectionGoal};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
@ -22,8 +22,8 @@ struct Paste {
|
||||
|
||||
impl_actions!(vim, [Paste]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(paste);
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(paste);
|
||||
}
|
||||
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
|
@ -5,14 +5,14 @@ use crate::{
|
||||
visual::visual_motion,
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, Action, AppContext, WindowContext};
|
||||
use gpui::{actions, Action, ViewContext, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat,]);
|
||||
actions!(vim, [Repeat, EndRepeat]);
|
||||
|
||||
fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
// skip so that we don't leave the character palette open
|
||||
if editor::ShowCharacterPalette.id() == action.id() {
|
||||
if editor::ShowCharacterPalette.partial_eq(&**action) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@ -21,14 +21,14 @@ fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if super::InsertBefore.id() == action.id()
|
||||
|| super::InsertAfter.id() == action.id()
|
||||
|| super::InsertFirstNonWhitespace.id() == action.id()
|
||||
|| super::InsertEndOfLine.id() == action.id()
|
||||
if super::InsertBefore.partial_eq(&**action)
|
||||
|| super::InsertAfter.partial_eq(&**action)
|
||||
|| super::InsertFirstNonWhitespace.partial_eq(&**action)
|
||||
|| super::InsertEndOfLine.partial_eq(&**action)
|
||||
{
|
||||
Some(super::InsertBefore.boxed_clone())
|
||||
} else if super::InsertLineAbove.id() == action.id()
|
||||
|| super::InsertLineBelow.id() == action.id()
|
||||
} else if super::InsertLineAbove.partial_eq(&**action)
|
||||
|| super::InsertLineBelow.partial_eq(&**action)
|
||||
{
|
||||
Some(super::InsertLineBelow.boxed_clone())
|
||||
} else {
|
||||
@ -39,15 +39,15 @@ fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_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| repeat(cx, false));
|
||||
workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
|
||||
}
|
||||
|
||||
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
@ -142,7 +142,7 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
// 3 times, instead it inserts the content thrice at the insert position.
|
||||
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
|
||||
if let Some(ReplayableAction::Action(action)) = actions.last() {
|
||||
if action.id() == NormalBefore.id() {
|
||||
if NormalBefore.partial_eq(&**action) {
|
||||
actions.pop();
|
||||
}
|
||||
}
|
||||
@ -166,9 +166,8 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
let window = cx.window_handle();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})?;
|
||||
@ -176,9 +175,7 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
window.update(&mut cx, |_, cx| cx.dispatch_action(action))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
@ -194,22 +191,18 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.show_local_selections = true;
|
||||
})?;
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone()))
|
||||
})
|
||||
.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 gpui::InputHandler;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
@ -217,7 +210,7 @@ mod test {
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// "o"
|
||||
@ -226,38 +219,32 @@ mod test {
|
||||
.await;
|
||||
cx.assert_shared_state("hello\nworlˇd").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
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;
|
||||
deterministic.run_until_parked();
|
||||
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;
|
||||
deterministic.run_until_parked();
|
||||
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;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE ˇquick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["3", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE QUIˇck brown fox").await;
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("THE QUICK ˇbrown fox").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("hˇllo", Mode::Normal);
|
||||
@ -271,15 +258,12 @@ mod test {
|
||||
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,
|
||||
) {
|
||||
async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
|
||||
VimTestContext::init(cx);
|
||||
let cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
@ -340,7 +324,6 @@ mod test {
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["j", "."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.second!
|
||||
@ -352,7 +335,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// single-line (3 columns)
|
||||
@ -371,7 +354,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "w", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o quick brown
|
||||
fox ˇops over
|
||||
@ -379,7 +361,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["f", "r", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o quick brown
|
||||
fox ops oveˇothe lazy dog"
|
||||
@ -404,7 +385,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the ˇumps over
|
||||
fox jumps over
|
||||
@ -412,14 +392,12 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["w", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the umps ˇumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the umps umps over
|
||||
the ˇog"
|
||||
@ -442,7 +420,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"othe quick brown
|
||||
ofoxˇo jumps over
|
||||
@ -466,7 +443,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o
|
||||
ˇo
|
||||
@ -476,10 +452,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_motion_counts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
@ -496,7 +469,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
" brown
|
||||
ˇ over
|
||||
@ -504,7 +476,6 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "2", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
" brown
|
||||
over
|
||||
@ -514,15 +485,12 @@ mod test {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_record_interrupted(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape"]);
|
||||
cx.simulate_keystrokes(["escape"]);
|
||||
cx.assert_state("ˇjhello\n", Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
@ -4,29 +4,29 @@ use editor::{
|
||||
scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN},
|
||||
DisplayPoint, Editor,
|
||||
};
|
||||
use gpui::{actions, AppContext, ViewContext};
|
||||
use gpui::{actions, ViewContext};
|
||||
use language::Bias;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown,]
|
||||
[LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &LineDown, cx| {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, _: &LineDown, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &LineUp, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &LineUp, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &PageDown, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &PageDown, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &PageUp, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &PageUp, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &ScrollDown, cx| {
|
||||
scroll(cx, true, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(c)
|
||||
@ -35,7 +35,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &ScrollUp, cx| {
|
||||
scroll(cx, true, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(-c)
|
||||
@ -114,7 +114,7 @@ mod test {
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use gpui::geometry::vector::vec2f;
|
||||
use gpui::{point, px, size, Context};
|
||||
use indoc::indoc;
|
||||
use language::Point;
|
||||
|
||||
@ -122,10 +122,27 @@ mod test {
|
||||
async fn test_scroll(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let (line_height, visible_line_count) = cx.editor(|editor, cx| {
|
||||
(
|
||||
editor
|
||||
.style()
|
||||
.unwrap()
|
||||
.text
|
||||
.line_height_in_pixels(cx.rem_size()),
|
||||
editor.visible_line_count().unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = cx.window;
|
||||
let line_height =
|
||||
cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx);
|
||||
let margin = cx
|
||||
.update_window(window, |_, cx| {
|
||||
cx.viewport_size().height - line_height * visible_line_count
|
||||
})
|
||||
.unwrap();
|
||||
cx.simulate_window_resize(
|
||||
cx.window,
|
||||
size(px(1000.), margin + 8. * line_height - px(1.0)),
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc!(
|
||||
@ -147,29 +164,29 @@ mod test {
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-e"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.))
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 1.))
|
||||
});
|
||||
cx.simulate_keystrokes(["2", "ctrl-e"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.))
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-y"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 2.))
|
||||
});
|
||||
|
||||
// does not select in normal mode
|
||||
cx.simulate_keystrokes(["g", "g"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-d"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.0));
|
||||
assert_eq!(
|
||||
editor.selections.newest(cx).range(),
|
||||
Point::new(6, 0)..Point::new(6, 0)
|
||||
@ -179,11 +196,11 @@ mod test {
|
||||
// does select in visual mode
|
||||
cx.simulate_keystrokes(["g", "g"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["v", "ctrl-d"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.0));
|
||||
assert_eq!(
|
||||
editor.selections.newest(cx).range(),
|
||||
Point::new(0, 0)..Point::new(6, 1)
|
||||
|
@ -1,7 +1,7 @@
|
||||
use gpui::{actions, impl_actions, AppContext, ViewContext};
|
||||
use gpui::{actions, impl_actions, ViewContext};
|
||||
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{searchable::Direction, Pane, Workspace};
|
||||
use workspace::{searchable::Direction, Workspace};
|
||||
|
||||
use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
|
||||
|
||||
@ -44,21 +44,21 @@ struct Replacement {
|
||||
is_case_sensitive: bool,
|
||||
}
|
||||
|
||||
actions!(vim, [SearchSubmit]);
|
||||
impl_actions!(
|
||||
vim,
|
||||
[MoveToNext, MoveToPrev, Search, FindCommand, ReplaceCommand]
|
||||
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
|
||||
);
|
||||
actions!(vim, [SearchSubmit]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(move_to_next);
|
||||
cx.add_action(move_to_prev);
|
||||
cx.add_action(search);
|
||||
cx.add_action(search_submit);
|
||||
cx.add_action(search_deploy);
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(move_to_next);
|
||||
workspace.register_action(move_to_prev);
|
||||
workspace.register_action(search);
|
||||
workspace.register_action(search_submit);
|
||||
workspace.register_action(search_deploy);
|
||||
|
||||
cx.add_action(find_command);
|
||||
cx.add_action(replace_command);
|
||||
workspace.register_action(find_command);
|
||||
workspace.register_action(replace_command);
|
||||
}
|
||||
|
||||
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
|
||||
@ -106,9 +106,9 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
|
||||
}
|
||||
|
||||
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
||||
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
|
||||
fn search_deploy(_: &mut Workspace, _: &buffer_search::Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
|
||||
cx.propagate_action();
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
|
||||
@ -347,58 +347,50 @@ fn parse_replace_all(query: &str) -> Replacement {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::DisplayPoint;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_next(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["#"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["#"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["2", "*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["g", "*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["g", "#"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||
@ -414,11 +406,11 @@ mod test {
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
cx.update_view(search_bar, |bar, cx| {
|
||||
assert_eq!(bar.query(cx), "cc");
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_text_background_highlights(cx);
|
||||
@ -440,51 +432,41 @@ mod test {
|
||||
|
||||
// ?<enter> to go to previous
|
||||
cx.simulate_keystrokes(["?", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["?", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
||||
|
||||
// /<enter> to go to next
|
||||
cx.simulate_keystrokes(["/", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||
|
||||
// ?{search}<enter> to search backwards
|
||||
cx.simulate_keystrokes(["?", "b", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||
|
||||
// works with counts
|
||||
cx.simulate_keystrokes(["4", "/", "c"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
|
||||
|
||||
// check that searching resumes from cursor, not previous match
|
||||
cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "d"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
|
||||
cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
|
||||
cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "b"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_non_vim_search(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.set_state("ˇone one one one", Mode::Normal);
|
||||
cx.simulate_keystrokes(["cmd-f"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.assert_editor_state("«oneˇ» one one one");
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
|
@ -1,5 +1,5 @@
|
||||
use editor::movement;
|
||||
use gpui::{actions, AppContext, WindowContext};
|
||||
use gpui::{actions, ViewContext, WindowContext};
|
||||
use language::Point;
|
||||
use workspace::Workspace;
|
||||
|
||||
@ -7,8 +7,8 @@ use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
|
||||
|
||||
actions!(vim, [Substitute, SubstituteLine]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let count = vim.take_count(cx);
|
||||
@ -16,7 +16,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
})
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
||||
workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||
|
@ -6,7 +6,7 @@ use editor::{
|
||||
movement::{self, FindRange},
|
||||
Bias, CharKind, DisplayPoint,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
||||
use gpui::{actions, impl_actions, ViewContext, WindowContext};
|
||||
use language::Selection;
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
@ -34,6 +34,8 @@ struct Word {
|
||||
ignore_punctuation: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [Word]);
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
@ -48,25 +50,36 @@ actions!(
|
||||
AngleBrackets
|
||||
]
|
||||
);
|
||||
impl_actions!(vim, [Word]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(
|
||||
|_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
|
||||
object(Object::Word { ignore_punctuation }, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| object(Object::DoubleQuotes, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Parentheses, cx: _| object(Object::Parentheses, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
|
||||
workspace
|
||||
.register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
|
||||
workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
|
||||
workspace
|
||||
.register_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
|
||||
workspace.register_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| {
|
||||
object(Object::DoubleQuotes, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &Parentheses, cx: _| {
|
||||
object(Object::Parentheses, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
|
||||
object(Object::SquareBrackets, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &VerticalBars, cx: _| object(Object::VerticalBars, cx));
|
||||
workspace.register_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| {
|
||||
object(Object::CurlyBrackets, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| {
|
||||
object(Object::AngleBrackets, cx)
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
|
||||
object(Object::VerticalBars, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn object(object: Object, cx: &mut WindowContext) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{keymap_matcher::KeymapContext, Action};
|
||||
use gpui::{Action, KeyContext};
|
||||
use language::CursorShape;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::searchable::Direction;
|
||||
@ -167,10 +167,10 @@ impl EditorState {
|
||||
self.operator_stack.last().copied()
|
||||
}
|
||||
|
||||
pub fn keymap_context_layer(&self) -> KeymapContext {
|
||||
let mut context = KeymapContext::default();
|
||||
context.add_identifier("VimEnabled");
|
||||
context.add_key(
|
||||
pub fn keymap_context_layer(&self) -> KeyContext {
|
||||
let mut context = KeyContext::default();
|
||||
context.add("VimEnabled");
|
||||
context.set(
|
||||
"vim_mode",
|
||||
match self.mode {
|
||||
Mode::Normal => "normal",
|
||||
@ -180,24 +180,24 @@ impl EditorState {
|
||||
);
|
||||
|
||||
if self.vim_controlled() {
|
||||
context.add_identifier("VimControl");
|
||||
context.add("VimControl");
|
||||
}
|
||||
|
||||
if self.active_operator().is_none() && self.pre_count.is_some()
|
||||
|| self.active_operator().is_some() && self.post_count.is_some()
|
||||
{
|
||||
context.add_identifier("VimCount");
|
||||
context.add("VimCount");
|
||||
}
|
||||
|
||||
let active_operator = self.active_operator();
|
||||
|
||||
if let Some(active_operator) = active_operator {
|
||||
for context_flag in active_operator.context_flags().into_iter() {
|
||||
context.add_identifier(*context_flag);
|
||||
context.add(*context_flag);
|
||||
}
|
||||
}
|
||||
|
||||
context.add_key(
|
||||
context.set(
|
||||
"vim_operator",
|
||||
active_operator.map(|op| op.id()).unwrap_or_else(|| "none"),
|
||||
);
|
||||
|
@ -3,8 +3,6 @@ mod neovim_backed_test_context;
|
||||
mod neovim_connection;
|
||||
mod vim_test_context;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use command_palette::CommandPalette;
|
||||
use editor::DisplayPoint;
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
@ -96,7 +94,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
cx.update_view(search_bar, |bar, cx| {
|
||||
assert_eq!(bar.query(cx), "");
|
||||
})
|
||||
}
|
||||
@ -149,9 +147,10 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
|
||||
cx.set_state("aˇbc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["i", "cmd-shift-p"]);
|
||||
|
||||
assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
|
||||
assert!(cx.workspace(|workspace, cx| workspace.active_modal::<CommandPalette>(cx).is_some()));
|
||||
cx.simulate_keystroke("escape");
|
||||
assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
|
||||
cx.run_until_parked();
|
||||
assert!(!cx.workspace(|workspace, cx| workspace.active_modal::<CommandPalette>(cx).is_some()));
|
||||
cx.assert_state("aˇbc\n", Mode::Insert);
|
||||
}
|
||||
|
||||
@ -182,7 +181,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
cx.update_view(search_bar, |bar, cx| {
|
||||
assert_eq!(bar.query(cx), "cc");
|
||||
});
|
||||
|
||||
@ -204,12 +203,8 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_status_indicator(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
async fn test_status_indicator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let mode_indicator = cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
@ -225,7 +220,6 @@ async fn test_status_indicator(
|
||||
|
||||
// shows the correct mode
|
||||
cx.simulate_keystrokes(["i"]);
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Some(Mode::Insert)
|
||||
@ -233,7 +227,6 @@ async fn test_status_indicator(
|
||||
|
||||
// shows even in search
|
||||
cx.simulate_keystrokes(["escape", "v", "/"]);
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Some(Mode::Visual)
|
||||
@ -241,7 +234,7 @@ async fn test_status_indicator(
|
||||
|
||||
// hides if vim mode is disabled
|
||||
cx.disable_vim();
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
|
||||
@ -249,7 +242,7 @@ async fn test_status_indicator(
|
||||
});
|
||||
|
||||
cx.enable_vim();
|
||||
deterministic.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
|
||||
|
@ -1,4 +1,5 @@
|
||||
use editor::scroll::VERTICAL_SCROLL_MARGIN;
|
||||
use editor::{scroll::VERTICAL_SCROLL_MARGIN, test::editor_test_context::ContextHandle};
|
||||
use gpui::{px, size, Context};
|
||||
use indoc::indoc;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
@ -7,7 +8,6 @@ use std::{
|
||||
};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{geometry::vector::vec2f, ContextHandle};
|
||||
use language::language_settings::{AllLanguageSettings, SoftWrap};
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
@ -158,11 +158,28 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
.await;
|
||||
// +2 to account for the vim command UI at the bottom.
|
||||
self.neovim.set_option(&format!("lines={}", rows + 2)).await;
|
||||
let window = self.window;
|
||||
let line_height =
|
||||
self.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
let (line_height, visible_line_count) = self.editor(|editor, cx| {
|
||||
(
|
||||
editor
|
||||
.style()
|
||||
.unwrap()
|
||||
.text
|
||||
.line_height_in_pixels(cx.rem_size()),
|
||||
editor.visible_line_count().unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
window.simulate_resize(vec2f(1000., (rows as f32) * line_height), &mut self.cx);
|
||||
let window = self.window;
|
||||
let margin = self
|
||||
.update_window(window, |_, cx| {
|
||||
cx.viewport_size().height - line_height * visible_line_count
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.simulate_window_resize(
|
||||
self.window,
|
||||
size(px(1000.), margin + (rows as f32) * line_height),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn set_neovim_option(&mut self, option: &str) {
|
||||
@ -211,12 +228,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
|
||||
pub async fn assert_shared_clipboard(&mut self, text: &str) {
|
||||
let neovim = self.neovim.read_register('"').await;
|
||||
let editor = self
|
||||
.platform()
|
||||
.read_from_clipboard()
|
||||
.unwrap()
|
||||
.text()
|
||||
.clone();
|
||||
let editor = self.read_from_clipboard().unwrap().text().clone();
|
||||
|
||||
if text == neovim && text == editor {
|
||||
return;
|
||||
|
@ -10,7 +10,7 @@ use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use gpui::keymap_matcher::Keystroke;
|
||||
use gpui::Keystroke;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use language::Point;
|
||||
@ -116,16 +116,24 @@ impl NeovimConnection {
|
||||
keystroke.key = "lt".to_string()
|
||||
}
|
||||
|
||||
let special = keystroke.shift
|
||||
|| keystroke.ctrl
|
||||
|| keystroke.alt
|
||||
|| keystroke.cmd
|
||||
let special = keystroke.modifiers.shift
|
||||
|| keystroke.modifiers.control
|
||||
|| keystroke.modifiers.alt
|
||||
|| keystroke.modifiers.command
|
||||
|| keystroke.key.len() > 1;
|
||||
let start = if special { "<" } else { "" };
|
||||
let shift = if keystroke.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.ctrl { "C-" } else { "" };
|
||||
let alt = if keystroke.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.cmd { "D-" } else { "" };
|
||||
let shift = if keystroke.modifiers.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.modifiers.control {
|
||||
"C-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let alt = if keystroke.modifiers.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.modifiers.command {
|
||||
"D-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let end = if special { ">" } else { "" };
|
||||
|
||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||
|
@ -4,9 +4,9 @@ use editor::test::{
|
||||
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
|
||||
};
|
||||
use futures::Future;
|
||||
use gpui::ContextHandle;
|
||||
use gpui::{Context, View, VisualContext};
|
||||
use lsp::request;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
||||
@ -15,12 +15,28 @@ pub struct VimTestContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> VimTestContext<'a> {
|
||||
pub fn init(cx: &mut gpui::TestAppContext) {
|
||||
if cx.has_global::<Vim>() {
|
||||
dbg!("OOPS");
|
||||
return;
|
||||
}
|
||||
cx.update(|cx| {
|
||||
search::init(cx);
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
command_palette::init(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
|
||||
Self::init(cx);
|
||||
let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
|
||||
Self::new_with_lsp(lsp, enabled)
|
||||
}
|
||||
|
||||
pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
|
||||
Self::init(cx);
|
||||
Self::new_with_lsp(
|
||||
EditorLspTestContext::new_typescript(Default::default(), cx).await,
|
||||
true,
|
||||
@ -28,12 +44,6 @@ impl<'a> VimTestContext<'a> {
|
||||
}
|
||||
|
||||
pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
|
||||
cx.update(|cx| {
|
||||
search::init(cx);
|
||||
crate::init(cx);
|
||||
command_palette::init(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
@ -47,14 +57,15 @@ impl<'a> VimTestContext<'a> {
|
||||
observe_keystrokes(cx);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
let buffer_search_bar = cx.new_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
// todo!();
|
||||
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
// toolbar.add_item(project_search_bar, cx);
|
||||
})
|
||||
});
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
let vim_mode_indicator = cx.add_view(ModeIndicator::new);
|
||||
let vim_mode_indicator = cx.new_view(ModeIndicator::new);
|
||||
status_bar.add_right_item(vim_mode_indicator, cx);
|
||||
});
|
||||
});
|
||||
@ -62,11 +73,21 @@ impl<'a> VimTestContext<'a> {
|
||||
Self { cx }
|
||||
}
|
||||
|
||||
pub fn workspace<F, T>(&mut self, read: F) -> T
|
||||
pub fn update_view<F, T, R>(&mut self, view: View<T>, update: F) -> R
|
||||
where
|
||||
F: FnOnce(&Workspace, &ViewContext<Workspace>) -> T,
|
||||
T: 'static,
|
||||
F: FnOnce(&mut T, &mut ViewContext<T>) -> R + 'static,
|
||||
{
|
||||
self.cx.workspace.read_with(self.cx.cx.cx, read)
|
||||
let window = self.window.clone();
|
||||
self.update_window(window, move |_, cx| view.update(cx, update))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.cx.update_workspace(update)
|
||||
}
|
||||
|
||||
pub fn enable_vim(&mut self) {
|
||||
@ -94,16 +115,16 @@ impl<'a> VimTestContext<'a> {
|
||||
.read(|cx| cx.global::<Vim>().state().operator_stack.last().copied())
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||
let window = self.window;
|
||||
let context_handle = self.cx.set_state(text);
|
||||
window.update(self.cx.cx.cx, |cx| {
|
||||
self.cx.set_state(text);
|
||||
self.update_window(window, |_, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(mode, true, cx);
|
||||
})
|
||||
});
|
||||
self.cx.foreground().run_until_parked();
|
||||
context_handle
|
||||
})
|
||||
.unwrap();
|
||||
self.cx.cx.cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
@ -1,5 +1,3 @@
|
||||
#![allow(unused)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
@ -17,17 +15,17 @@ mod visual;
|
||||
use anyhow::Result;
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use command_palette::CommandPaletteInterceptor;
|
||||
use editor::{movement, Editor, EditorMode, Event};
|
||||
use editor::{movement, Editor, EditorEvent, EditorMode};
|
||||
use gpui::{
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
|
||||
AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
actions, impl_actions, Action, AppContext, EntityId, KeyContext, Subscription, View,
|
||||
ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::normal_replace;
|
||||
use serde::Deserialize;
|
||||
use settings::{update_settings_file, Setting, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use visual::{visual_block_motion, visual_replace};
|
||||
@ -50,83 +48,86 @@ actions!(
|
||||
vim,
|
||||
[Tab, Enter, Object, InnerObject, FindForward, FindBackward]
|
||||
);
|
||||
// in the workspace namespace so it's not filtered out when vim is disabled.
|
||||
actions!(workspace, [ToggleVimMode]);
|
||||
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum VimEvent {
|
||||
ModeChanged { mode: Mode },
|
||||
}
|
||||
impl_actions!(vim, [SwitchMode, PushOperator, Number]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(Vim::default());
|
||||
settings::register::<VimModeSetting>(cx);
|
||||
VimModeSetting::register(cx);
|
||||
|
||||
editor_events::init(cx);
|
||||
normal::init(cx);
|
||||
visual::init(cx);
|
||||
insert::init(cx);
|
||||
object::init(cx);
|
||||
motion::init(cx);
|
||||
command::init(cx);
|
||||
|
||||
// Vim Actions
|
||||
cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
|
||||
Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx))
|
||||
});
|
||||
cx.add_action(
|
||||
|_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
|
||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||
},
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
|
||||
Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
|
||||
Vim::active_editor_input_ignored(" ".into(), cx)
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Enter, cx| {
|
||||
Vim::active_editor_input_ignored("\n".into(), cx)
|
||||
});
|
||||
|
||||
cx.add_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let currently_enabled = settings::get::<VimModeSetting>(cx).0;
|
||||
update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
|
||||
*setting = Some(!currently_enabled)
|
||||
})
|
||||
});
|
||||
cx.observe_new_views(|workspace: &mut Workspace, cx| register(workspace, cx))
|
||||
.detach();
|
||||
|
||||
// Any time settings change, update vim mode to match. The Vim struct
|
||||
// will be initialized as disabled by default, so we filter its commands
|
||||
// out when starting up.
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.hidden_namespaces.insert("vim");
|
||||
});
|
||||
cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
|
||||
vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
|
||||
vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
|
||||
});
|
||||
cx.observe_global::<SettingsStore, _>(|cx| {
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
|
||||
vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
|
||||
vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
|
||||
if result == &MatchResult::Pending {
|
||||
return true;
|
||||
fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
|
||||
Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx))
|
||||
});
|
||||
workspace.register_action(
|
||||
|_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
|
||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||
},
|
||||
);
|
||||
workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| {
|
||||
Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
|
||||
});
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &Tab, cx| {
|
||||
Vim::active_editor_input_ignored(" ".into(), cx)
|
||||
});
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &Enter, cx| {
|
||||
Vim::active_editor_input_ignored("\n".into(), cx)
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let currently_enabled = VimModeSetting::get_global(cx).0;
|
||||
update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
|
||||
*setting = Some(!currently_enabled)
|
||||
})
|
||||
});
|
||||
|
||||
normal::register(workspace, cx);
|
||||
insert::register(workspace, cx);
|
||||
motion::register(workspace, cx);
|
||||
command::register(workspace, cx);
|
||||
object::register(workspace, cx);
|
||||
visual::register(workspace, cx);
|
||||
}
|
||||
if let Some(handled_by) = handled_by {
|
||||
|
||||
pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
cx.observe_keystrokes(|keystroke_event, cx| {
|
||||
if let Some(action) = keystroke_event
|
||||
.action
|
||||
.as_ref()
|
||||
.map(|action| action.boxed_clone())
|
||||
{
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(handled_by.boxed_clone()));
|
||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
@ -136,9 +137,11 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
});
|
||||
|
||||
// Keystroke is handled by the vim system, so continue forward
|
||||
if handled_by.namespace() == "vim" {
|
||||
return true;
|
||||
if action.name().starts_with("vim::") {
|
||||
return;
|
||||
}
|
||||
} else if cx.has_pending_keystrokes() {
|
||||
return;
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, cx| match vim.active_operator() {
|
||||
@ -150,24 +153,23 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
true
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Vim {
|
||||
active_editor: Option<WeakViewHandle<Editor>>,
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
editor_subscription: Option<Subscription>,
|
||||
enabled: bool,
|
||||
editor_states: HashMap<usize, EditorState>,
|
||||
editor_states: HashMap<EntityId, EditorState>,
|
||||
workspace_state: WorkspaceState,
|
||||
default_state: EditorState,
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
fn read(cx: &mut AppContext) -> &Self {
|
||||
cx.default_global()
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
fn update<F, S>(cx: &mut WindowContext, update: F) -> S
|
||||
@ -177,21 +179,21 @@ impl Vim {
|
||||
cx.update_global(update)
|
||||
}
|
||||
|
||||
fn set_active_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut WindowContext) {
|
||||
fn set_active_editor(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
|
||||
self.active_editor = Some(editor.clone().downgrade());
|
||||
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
|
||||
Event::SelectionsChanged { local: true } => {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let editor = editor.read(cx);
|
||||
if editor.leader_peer_id().is_none() {
|
||||
let newest = editor.selections.newest::<usize>(cx);
|
||||
local_selections_changed(newest, cx);
|
||||
}
|
||||
}
|
||||
Event::InputIgnored { text } => {
|
||||
EditorEvent::InputIgnored { text } => {
|
||||
Vim::active_editor_input_ignored(text.clone(), cx);
|
||||
Vim::record_insertion(text, None, cx)
|
||||
}
|
||||
Event::InputHandled {
|
||||
EditorEvent::InputHandled {
|
||||
text,
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
|
||||
@ -242,7 +244,7 @@ impl Vim {
|
||||
cx: &mut WindowContext,
|
||||
update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
|
||||
) -> Option<S> {
|
||||
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
||||
let editor = self.active_editor.clone()?.upgrade()?;
|
||||
Some(editor.update(cx, update))
|
||||
}
|
||||
|
||||
@ -254,7 +256,8 @@ impl Vim {
|
||||
|
||||
let selections = self
|
||||
.active_editor
|
||||
.and_then(|editor| editor.upgrade(cx))
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| {
|
||||
let editor = editor.read(cx);
|
||||
(
|
||||
@ -323,8 +326,6 @@ impl Vim {
|
||||
self.take_count(cx);
|
||||
}
|
||||
|
||||
cx.emit_global(VimEvent::ModeChanged { mode });
|
||||
|
||||
// Sync editor settings like clip mode
|
||||
self.sync_vim_settings(cx);
|
||||
|
||||
@ -477,7 +478,7 @@ impl Vim {
|
||||
if self.enabled != enabled {
|
||||
self.enabled = enabled;
|
||||
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
if self.enabled {
|
||||
filter.hidden_namespaces.remove("vim");
|
||||
} else {
|
||||
@ -491,11 +492,13 @@ impl Vim {
|
||||
let _ = cx.remove_global::<CommandPaletteInterceptor>();
|
||||
}
|
||||
|
||||
cx.update_active_window(|cx| {
|
||||
if let Some(active_window) = cx.active_window() {
|
||||
active_window
|
||||
.update(cx, |root_view, cx| {
|
||||
if self.enabled {
|
||||
let active_editor = cx
|
||||
.root_view()
|
||||
.downcast_ref::<Workspace>()
|
||||
let active_editor = root_view
|
||||
.downcast::<Workspace>()
|
||||
.ok()
|
||||
.and_then(|workspace| workspace.read(cx).active_item(cx))
|
||||
.and_then(|item| item.downcast::<Editor>());
|
||||
if let Some(active_editor) = active_editor {
|
||||
@ -504,13 +507,15 @@ impl Vim {
|
||||
self.switch_mode(Mode::Normal, false, cx);
|
||||
}
|
||||
self.sync_vim_settings(cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &EditorState {
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
if let Some(state) = self.editor_states.get(&active_editor.id()) {
|
||||
if let Some(state) = self.editor_states.get(&active_editor.entity_id()) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@ -523,7 +528,7 @@ impl Vim {
|
||||
let ret = func(&mut state);
|
||||
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
self.editor_states.insert(active_editor.id(), state);
|
||||
self.editor_states.insert(active_editor.entity_id(), state);
|
||||
}
|
||||
|
||||
ret
|
||||
@ -564,8 +569,8 @@ impl Vim {
|
||||
// This is a bit of a hack, but currently the search crate does not depend on vim,
|
||||
// and it seems nice to keep it that way.
|
||||
if self.enabled {
|
||||
let mut context = KeymapContext::default();
|
||||
context.add_identifier("VimEnabled");
|
||||
let mut context = KeyContext::default();
|
||||
context.add("VimEnabled");
|
||||
editor.set_keymap_context_layer::<Self>(context, cx)
|
||||
} else {
|
||||
editor.remove_keymap_context_layer::<Self>(cx);
|
||||
@ -573,7 +578,7 @@ impl Vim {
|
||||
}
|
||||
}
|
||||
|
||||
impl Setting for VimModeSetting {
|
||||
impl Settings for VimModeSetting {
|
||||
const KEY: Option<&'static str> = Some("vim_mode");
|
||||
|
||||
type FileContent = Option<bool>;
|
||||
@ -581,7 +586,7 @@ impl Setting for VimModeSetting {
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &AppContext,
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
|
||||
default_value.ok_or_else(Self::missing_default)?,
|
||||
|
@ -8,7 +8,7 @@ use editor::{
|
||||
scroll::autoscroll::Autoscroll,
|
||||
Bias, DisplayPoint, Editor,
|
||||
};
|
||||
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
||||
use gpui::{actions, ViewContext, WindowContext};
|
||||
use language::{Selection, SelectionGoal};
|
||||
use workspace::Workspace;
|
||||
|
||||
@ -34,24 +34,28 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
|
||||
toggle_mode(Mode::Visual, cx)
|
||||
});
|
||||
cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
|
||||
toggle_mode(Mode::VisualLine, cx)
|
||||
});
|
||||
cx.add_action(
|
||||
workspace.register_action(
|
||||
|_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
|
||||
toggle_mode(Mode::VisualBlock, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(other_end);
|
||||
cx.add_action(delete);
|
||||
cx.add_action(yank);
|
||||
workspace.register_action(other_end);
|
||||
workspace.register_action(delete);
|
||||
workspace.register_action(yank);
|
||||
|
||||
cx.add_action(select_next);
|
||||
cx.add_action(select_previous);
|
||||
workspace.register_action(|workspace, action, cx| {
|
||||
select_next(workspace, action, cx).ok();
|
||||
});
|
||||
workspace.register_action(|workspace, action, cx| {
|
||||
select_previous(workspace, action, cx).ok();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
@ -146,13 +150,13 @@ pub fn visual_block_motion(
|
||||
let mut head = s.newest_anchor().head().to_display_point(map);
|
||||
let mut tail = s.oldest_anchor().tail().to_display_point(map);
|
||||
|
||||
let mut head_x = map.x_for_point(head, &text_layout_details);
|
||||
let mut tail_x = map.x_for_point(tail, &text_layout_details);
|
||||
let mut head_x = map.x_for_display_point(head, &text_layout_details);
|
||||
let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
|
||||
|
||||
let (start, end) = match s.newest_anchor().goal {
|
||||
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
|
||||
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
|
||||
_ => (tail_x, head_x),
|
||||
_ => (tail_x.0, head_x.0),
|
||||
};
|
||||
let mut goal = SelectionGoal::HorizontalRange { start, end };
|
||||
|
||||
@ -165,19 +169,19 @@ pub fn visual_block_motion(
|
||||
return;
|
||||
};
|
||||
head = new_head;
|
||||
head_x = map.x_for_point(head, &text_layout_details);
|
||||
head_x = map.x_for_display_point(head, &text_layout_details);
|
||||
|
||||
let is_reversed = tail_x > head_x;
|
||||
if was_reversed && !is_reversed {
|
||||
tail = movement::saturating_left(map, tail);
|
||||
tail_x = map.x_for_point(tail, &text_layout_details);
|
||||
tail_x = map.x_for_display_point(tail, &text_layout_details);
|
||||
} else if !was_reversed && is_reversed {
|
||||
tail = movement::saturating_right(map, tail);
|
||||
tail_x = map.x_for_point(tail, &text_layout_details);
|
||||
tail_x = map.x_for_display_point(tail, &text_layout_details);
|
||||
}
|
||||
if !is_reversed && !preserve_goal {
|
||||
head = movement::saturating_right(map, head);
|
||||
head_x = map.x_for_point(head, &text_layout_details);
|
||||
head_x = map.x_for_display_point(head, &text_layout_details);
|
||||
}
|
||||
|
||||
let positions = if is_reversed {
|
||||
@ -188,8 +192,8 @@ pub fn visual_block_motion(
|
||||
|
||||
if !preserve_goal {
|
||||
goal = SelectionGoal::HorizontalRange {
|
||||
start: positions.start,
|
||||
end: positions.end,
|
||||
start: positions.start.0,
|
||||
end: positions.end.0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -197,7 +201,7 @@ pub fn visual_block_motion(
|
||||
let mut row = tail.row();
|
||||
|
||||
loop {
|
||||
let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
|
||||
let layed_out_line = map.layout_row(row, &text_layout_details);
|
||||
let start = DisplayPoint::new(
|
||||
row,
|
||||
layed_out_line.closest_index_for_x(positions.start) as u32,
|
||||
@ -214,7 +218,7 @@ pub fn visual_block_motion(
|
||||
}
|
||||
}
|
||||
|
||||
if positions.start <= layed_out_line.width() {
|
||||
if positions.start <= layed_out_line.width {
|
||||
let selection = Selection {
|
||||
id: s.new_selection_id(),
|
||||
start: start.to_point(map),
|
||||
@ -749,7 +753,12 @@ mod test {
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.assert_clipboard_content(Some("The q"));
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard()
|
||||
.map(|item| item.text().clone())
|
||||
.unwrap(),
|
||||
"The q"
|
||||
);
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
|
@ -1,53 +0,0 @@
|
||||
[package]
|
||||
name = "vim2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/vim.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
itertools = "0.10"
|
||||
log.workspace = true
|
||||
|
||||
async-compat = { version = "0.2.1", "optional" = true }
|
||||
async-trait = { workspace = true, "optional" = true }
|
||||
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
|
||||
tokio = { version = "1.15", "optional" = true }
|
||||
serde_json.workspace = true
|
||||
|
||||
collections = { path = "../collections" }
|
||||
command_palette = { package = "command_palette2", path = "../command_palette2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2"}
|
||||
diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
|
||||
zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
@ -1,434 +0,0 @@
|
||||
use command_palette::CommandInterceptResult;
|
||||
use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive};
|
||||
use gpui::{impl_actions, Action, AppContext, ViewContext};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{SaveIntent, Workspace};
|
||||
|
||||
use crate::{
|
||||
motion::{EndOfDocument, Motion},
|
||||
normal::{
|
||||
move_cursor,
|
||||
search::{FindCommand, ReplaceCommand},
|
||||
JoinLines,
|
||||
},
|
||||
state::Mode,
|
||||
Vim,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct GoToLine {
|
||||
pub line: u32,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [GoToLine]);
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, action: &GoToLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
move_cursor(vim, Motion::StartOfDocument, Some(action.line as usize), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option<CommandInterceptResult> {
|
||||
// Note: this is a very poor simulation of vim's command palette.
|
||||
// In the future we should adjust it to handle parsing range syntax,
|
||||
// and then calling the appropriate commands with/without ranges.
|
||||
//
|
||||
// We also need to support passing arguments to commands like :w
|
||||
// (ideally with filename autocompletion).
|
||||
//
|
||||
// For now, you can only do a replace on the % range, and you can
|
||||
// only use a specific line number range to "go to line"
|
||||
while query.starts_with(":") {
|
||||
query = &query[1..];
|
||||
}
|
||||
|
||||
let (name, action) = match query {
|
||||
// save and quit
|
||||
"w" | "wr" | "wri" | "writ" | "write" => (
|
||||
"write",
|
||||
workspace::Save {
|
||||
save_intent: Some(SaveIntent::Save),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"w!" | "wr!" | "wri!" | "writ!" | "write!" => (
|
||||
"write!",
|
||||
workspace::Save {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"q" | "qu" | "qui" | "quit" => (
|
||||
"quit",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::Close),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"q!" | "qu!" | "qui!" | "quit!" => (
|
||||
"quit!",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::Skip),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"wq" => (
|
||||
"wq",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::Save),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"wq!" => (
|
||||
"wq!",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"x" | "xi" | "xit" | "exi" | "exit" => (
|
||||
"exit",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::SaveAll),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"x!" | "xi!" | "xit!" | "exi!" | "exit!" => (
|
||||
"exit!",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"up" | "upd" | "upda" | "updat" | "update" => (
|
||||
"update",
|
||||
workspace::Save {
|
||||
save_intent: Some(SaveIntent::SaveAll),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"wa" | "wal" | "wall" => (
|
||||
"wall",
|
||||
workspace::SaveAll {
|
||||
save_intent: Some(SaveIntent::SaveAll),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"wa!" | "wal!" | "wall!" => (
|
||||
"wall!",
|
||||
workspace::SaveAll {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => (
|
||||
"quitall",
|
||||
workspace::CloseAllItemsAndPanes {
|
||||
save_intent: Some(SaveIntent::Close),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => (
|
||||
"quitall!",
|
||||
workspace::CloseAllItemsAndPanes {
|
||||
save_intent: Some(SaveIntent::Skip),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"xa" | "xal" | "xall" => (
|
||||
"xall",
|
||||
workspace::CloseAllItemsAndPanes {
|
||||
save_intent: Some(SaveIntent::SaveAll),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"xa!" | "xal!" | "xall!" => (
|
||||
"xall!",
|
||||
workspace::CloseAllItemsAndPanes {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"wqa" | "wqal" | "wqall" => (
|
||||
"wqall",
|
||||
workspace::CloseAllItemsAndPanes {
|
||||
save_intent: Some(SaveIntent::SaveAll),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"wqa!" | "wqal!" | "wqall!" => (
|
||||
"wqall!",
|
||||
workspace::CloseAllItemsAndPanes {
|
||||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
"cq" | "cqu" | "cqui" | "cquit" | "cq!" | "cqu!" | "cqui!" | "cquit!" => {
|
||||
("cquit!", zed_actions::Quit.boxed_clone())
|
||||
}
|
||||
|
||||
// pane management
|
||||
"sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()),
|
||||
"vs" | "vsp" | "vspl" | "vspli" | "vsplit" => {
|
||||
("vsplit", workspace::SplitLeft.boxed_clone())
|
||||
}
|
||||
"new" => (
|
||||
"new",
|
||||
workspace::NewFileInDirection(workspace::SplitDirection::Up).boxed_clone(),
|
||||
),
|
||||
"vne" | "vnew" => (
|
||||
"vnew",
|
||||
workspace::NewFileInDirection(workspace::SplitDirection::Left).boxed_clone(),
|
||||
),
|
||||
"tabe" | "tabed" | "tabedi" | "tabedit" => ("tabedit", workspace::NewFile.boxed_clone()),
|
||||
"tabnew" => ("tabnew", workspace::NewFile.boxed_clone()),
|
||||
|
||||
"tabn" | "tabne" | "tabnex" | "tabnext" => {
|
||||
("tabnext", workspace::ActivateNextItem.boxed_clone())
|
||||
}
|
||||
"tabp" | "tabpr" | "tabpre" | "tabprev" | "tabprevi" | "tabprevio" | "tabpreviou"
|
||||
| "tabprevious" => ("tabprevious", workspace::ActivatePrevItem.boxed_clone()),
|
||||
"tabN" | "tabNe" | "tabNex" | "tabNext" => {
|
||||
("tabNext", workspace::ActivatePrevItem.boxed_clone())
|
||||
}
|
||||
"tabc" | "tabcl" | "tabclo" | "tabclos" | "tabclose" => (
|
||||
"tabclose",
|
||||
workspace::CloseActiveItem {
|
||||
save_intent: Some(SaveIntent::Close),
|
||||
}
|
||||
.boxed_clone(),
|
||||
),
|
||||
|
||||
// quickfix / loclist (merged together for now)
|
||||
"cl" | "cli" | "clis" | "clist" => ("clist", diagnostics::Deploy.boxed_clone()),
|
||||
"cc" => ("cc", editor::Hover.boxed_clone()),
|
||||
"ll" => ("ll", editor::Hover.boxed_clone()),
|
||||
"cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
|
||||
"lne" | "lnex" | "lnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
|
||||
|
||||
"cpr" | "cpre" | "cprev" | "cprevi" | "cprevio" | "cpreviou" | "cprevious" => {
|
||||
("cprevious", editor::GoToPrevDiagnostic.boxed_clone())
|
||||
}
|
||||
"cN" | "cNe" | "cNex" | "cNext" => ("cNext", editor::GoToPrevDiagnostic.boxed_clone()),
|
||||
"lp" | "lpr" | "lpre" | "lprev" | "lprevi" | "lprevio" | "lpreviou" | "lprevious" => {
|
||||
("lprevious", editor::GoToPrevDiagnostic.boxed_clone())
|
||||
}
|
||||
"lN" | "lNe" | "lNex" | "lNext" => ("lNext", editor::GoToPrevDiagnostic.boxed_clone()),
|
||||
|
||||
// modify the buffer (should accept [range])
|
||||
"j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()),
|
||||
"d" | "de" | "del" | "dele" | "delet" | "delete" | "dl" | "dell" | "delel" | "deletl"
|
||||
| "deletel" | "dp" | "dep" | "delp" | "delep" | "deletp" | "deletep" => {
|
||||
("delete", editor::DeleteLine.boxed_clone())
|
||||
}
|
||||
"sor" | "sor " | "sort" | "sort " => ("sort", SortLinesCaseSensitive.boxed_clone()),
|
||||
"sor i" | "sort i" => ("sort i", SortLinesCaseInsensitive.boxed_clone()),
|
||||
|
||||
// goto (other ranges handled under _ => )
|
||||
"$" => ("$", EndOfDocument.boxed_clone()),
|
||||
|
||||
_ => {
|
||||
if query.starts_with("/") || query.starts_with("?") {
|
||||
(
|
||||
query,
|
||||
FindCommand {
|
||||
query: query[1..].to_string(),
|
||||
backwards: query.starts_with("?"),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
} else if query.starts_with("%") {
|
||||
(
|
||||
query,
|
||||
ReplaceCommand {
|
||||
query: query.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
} else if let Ok(line) = query.parse::<u32>() {
|
||||
(query, GoToLine { line }.boxed_clone())
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let string = ":".to_owned() + name;
|
||||
let positions = generate_positions(&string, query);
|
||||
|
||||
Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
positions,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_positions(string: &str, query: &str) -> Vec<usize> {
|
||||
let mut positions = Vec::new();
|
||||
let mut chars = query.chars().into_iter();
|
||||
|
||||
let Some(mut current) = chars.next() else {
|
||||
return positions;
|
||||
};
|
||||
|
||||
for (i, c) in string.chars().enumerate() {
|
||||
if c == current {
|
||||
positions.push(i);
|
||||
if let Some(c) = chars.next() {
|
||||
current = c;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
positions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::test::{NeovimBackedTestContext, VimTestContext};
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_basics(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇa
|
||||
b
|
||||
c"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes([":", "j", "enter"]).await;
|
||||
|
||||
// hack: our cursor positionining after a join command is wrong
|
||||
cx.simulate_shared_keystrokes(["^"]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇa b
|
||||
c"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_goto(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇa
|
||||
b
|
||||
c"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes([":", "3", "enter"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
a
|
||||
b
|
||||
ˇc"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_replace(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇa
|
||||
b
|
||||
c"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes([":", "%", "s", "/", "b", "/", "d", "enter"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
a
|
||||
ˇd
|
||||
c"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes([
|
||||
":", "%", "s", ":", ".", ":", "\\", "0", "\\", "0", "enter",
|
||||
])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
aa
|
||||
dd
|
||||
ˇcc"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_search(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇa
|
||||
b
|
||||
a
|
||||
c"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes([":", "/", "b", "enter"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
a
|
||||
ˇb
|
||||
a
|
||||
c"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes([":", "?", "a", "enter"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇa
|
||||
b
|
||||
a
|
||||
c"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_write(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
let path = Path::new("/root/dir/file.rs");
|
||||
let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
|
||||
|
||||
cx.simulate_keystrokes(["i", "@", "escape"]);
|
||||
cx.simulate_keystrokes([":", "w", "enter"]);
|
||||
|
||||
assert_eq!(fs.load(&path).await.unwrap(), "@\n");
|
||||
|
||||
fs.as_fake()
|
||||
.write_file_internal(path, "oops\n".to_string())
|
||||
.unwrap();
|
||||
|
||||
// conflict!
|
||||
cx.simulate_keystrokes(["i", "@", "escape"]);
|
||||
cx.simulate_keystrokes([":", "w", "enter"]);
|
||||
assert!(cx.has_pending_prompt());
|
||||
// "Cancel"
|
||||
cx.simulate_prompt_answer(0);
|
||||
assert_eq!(fs.load(&path).await.unwrap(), "oops\n");
|
||||
assert!(!cx.has_pending_prompt());
|
||||
// force overwrite
|
||||
cx.simulate_keystrokes([":", "w", "!", "enter"]);
|
||||
assert!(!cx.has_pending_prompt());
|
||||
assert_eq!(fs.load(&path).await.unwrap(), "@@\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_quit(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystrokes([":", "n", "e", "w", "enter"]);
|
||||
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
|
||||
cx.simulate_keystrokes([":", "q", "enter"]);
|
||||
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
|
||||
cx.simulate_keystrokes([":", "n", "e", "w", "enter"]);
|
||||
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
|
||||
cx.simulate_keystrokes([":", "q", "a", "enter"]);
|
||||
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
use crate::Vim;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use gpui::{AppContext, Entity, EntityId, View, ViewContext, WindowContext};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|_, cx: &mut ViewContext<Editor>| {
|
||||
let editor = cx.view().clone();
|
||||
cx.subscribe(&editor, |_, editor, event: &EditorEvent, cx| match event {
|
||||
EditorEvent::Focused => cx.window_context().defer(|cx| focused(editor, cx)),
|
||||
EditorEvent::Blurred => cx.window_context().defer(|cx| blurred(editor, cx)),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let id = cx.view().entity_id();
|
||||
cx.on_release(move |_, _, cx| released(id, cx)).detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn focused(editor: View<Editor>, cx: &mut WindowContext) {
|
||||
if Vim::read(cx).active_editor.clone().is_some() {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |previously_active_editor, cx| {
|
||||
vim.unhook_vim_settings(previously_active_editor, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.set_active_editor(editor.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.recorded_actions.clear();
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor
|
||||
.upgrade()
|
||||
.is_some_and(|previous| previous == editor.clone())
|
||||
{
|
||||
vim.clear_operator(cx);
|
||||
vim.active_editor = None;
|
||||
vim.editor_subscription = None;
|
||||
}
|
||||
}
|
||||
|
||||
editor.update(cx, |editor, cx| vim.unhook_vim_settings(editor, cx))
|
||||
});
|
||||
}
|
||||
|
||||
fn released(entity_id: EntityId, cx: &mut AppContext) {
|
||||
cx.update_global(|vim: &mut Vim, _| {
|
||||
if vim
|
||||
.active_editor
|
||||
.as_ref()
|
||||
.is_some_and(|previous| previous.entity_id() == entity_id)
|
||||
{
|
||||
vim.active_editor = None;
|
||||
vim.editor_subscription = None;
|
||||
}
|
||||
vim.editor_states.remove(&entity_id)
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{test::VimTestContext, Vim};
|
||||
use editor::Editor;
|
||||
use gpui::{Context, Entity};
|
||||
use language::Buffer;
|
||||
|
||||
// regression test for blur called with a different active editor
|
||||
#[gpui::test]
|
||||
async fn test_blur_focus(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
|
||||
let window2 = cx.add_window(|cx| Editor::for_buffer(buffer, None, cx));
|
||||
let editor2 = cx
|
||||
.update(|cx| {
|
||||
window2.update(cx, |_, cx| {
|
||||
cx.focus_self();
|
||||
cx.view().clone()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let vim = Vim::read(cx);
|
||||
assert_eq!(
|
||||
vim.active_editor.as_ref().unwrap().entity_id(),
|
||||
editor2.entity_id(),
|
||||
)
|
||||
});
|
||||
|
||||
// no panic when blurring an editor in a different window.
|
||||
cx.update_editor(|editor1, cx| {
|
||||
editor1.handle_blur(cx);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
use crate::{normal::repeat, state::Mode, Vim};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::{actions, Action, ViewContext};
|
||||
use language::SelectionGoal;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [NormalBefore]);
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(normal_before);
|
||||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
let should_repeat = Vim::update(cx, |vim, cx| {
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
vim.stop_recording_immediately(action.boxed_clone());
|
||||
if count <= 1 || vim.workspace_state.replaying {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.cancel(&Default::default(), cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if should_repeat {
|
||||
repeat::repeat(cx, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.simulate_keystrokes(["T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("Testˇ");
|
||||
cx.simulate_keystroke("escape");
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_editor_state("Tesˇt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ-hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("h----ˇ-ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("---ˇ-h-----ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("----h-----ello--ˇ-\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("--ˇ-hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ--hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "."]).await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("-----ˇ---hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["1", "."]).await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
use gpui::{div, Element, Render, Subscription, ViewContext};
|
||||
use settings::SettingsStore;
|
||||
use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
|
||||
|
||||
use crate::{state::Mode, Vim};
|
||||
|
||||
pub struct ModeIndicator {
|
||||
pub mode: Option<Mode>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ModeIndicator {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let _subscriptions = vec![
|
||||
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)),
|
||||
cx.observe_global::<SettingsStore>(|this, cx| this.update_mode(cx)),
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
mode: None,
|
||||
_subscriptions,
|
||||
};
|
||||
this.update_mode(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_mode(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// Vim doesn't exist in some tests
|
||||
if !cx.has_global::<Vim>() {
|
||||
return;
|
||||
}
|
||||
|
||||
let vim = Vim::read(cx);
|
||||
if vim.enabled {
|
||||
self.mode = Some(vim.state().mode);
|
||||
} else {
|
||||
self.mode = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
if self.mode != Some(mode) {
|
||||
self.mode = Some(mode);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModeIndicator {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Some(mode) = self.mode.as_ref() else {
|
||||
return div().into_any();
|
||||
};
|
||||
|
||||
let text = match mode {
|
||||
Mode::Normal => "-- NORMAL --",
|
||||
Mode::Insert => "-- INSERT --",
|
||||
Mode::Visual => "-- VISUAL --",
|
||||
Mode::VisualLine => "-- VISUAL LINE --",
|
||||
Mode::VisualBlock => "-- VISUAL BLOCK --",
|
||||
};
|
||||
Label::new(text).size(LabelSize::Small).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ModeIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn ItemHandle>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,910 +0,0 @@
|
||||
mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod increment;
|
||||
mod paste;
|
||||
pub(crate) mod repeat;
|
||||
mod scroll;
|
||||
pub(crate) mod search;
|
||||
pub mod substitute;
|
||||
mod yank;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
motion::{self, first_non_whitespace, next_line_end, right, Motion},
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
Vim,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use editor::scroll::autoscroll::Autoscroll;
|
||||
use editor::{Bias, DisplayPoint};
|
||||
use gpui::{actions, ViewContext, WindowContext};
|
||||
use language::SelectionGoal;
|
||||
use log::error;
|
||||
use workspace::Workspace;
|
||||
|
||||
use self::{
|
||||
case::change_case,
|
||||
change::{change_motion, change_object},
|
||||
delete::{delete_motion, delete_object},
|
||||
yank::{yank_motion, yank_object},
|
||||
};
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
InsertAfter,
|
||||
InsertBefore,
|
||||
InsertFirstNonWhitespace,
|
||||
InsertEndOfLine,
|
||||
InsertLineAbove,
|
||||
InsertLineBelow,
|
||||
DeleteLeft,
|
||||
DeleteRight,
|
||||
ChangeToEndOfLine,
|
||||
DeleteToEndOfLine,
|
||||
Yank,
|
||||
YankLine,
|
||||
ChangeCase,
|
||||
JoinLines,
|
||||
]
|
||||
);
|
||||
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(insert_after);
|
||||
workspace.register_action(insert_before);
|
||||
workspace.register_action(insert_first_non_whitespace);
|
||||
workspace.register_action(insert_end_of_line);
|
||||
workspace.register_action(insert_line_above);
|
||||
workspace.register_action(insert_line_below);
|
||||
workspace.register_action(change_case);
|
||||
workspace.register_action(yank_line);
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(vim, Motion::Left, times, cx);
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(vim, Motion::Right, times, cx);
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let times = vim.take_count(cx);
|
||||
change_motion(
|
||||
vim,
|
||||
Motion::EndOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
times,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(
|
||||
vim,
|
||||
Motion::EndOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
times,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let mut times = vim.take_count(cx).unwrap_or(1);
|
||||
if vim.state().mode.is_visual() {
|
||||
times = 1;
|
||||
} else if times > 1 {
|
||||
// 2J joins two lines together (same as J or 1J)
|
||||
times -= 1;
|
||||
}
|
||||
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
for _ in 0..times {
|
||||
editor.join_lines(&Default::default(), cx)
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
paste::register(workspace, cx);
|
||||
repeat::register(workspace, cx);
|
||||
scroll::register(workspace, cx);
|
||||
search::register(workspace, cx);
|
||||
substitute::register(workspace, cx);
|
||||
increment::register(workspace, cx);
|
||||
}
|
||||
|
||||
pub fn normal_motion(
|
||||
motion: Motion,
|
||||
operator: Option<Operator>,
|
||||
times: Option<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
match operator {
|
||||
None => move_cursor(vim, motion, times, cx),
|
||||
Some(Operator::Change) => change_motion(vim, motion, times, cx),
|
||||
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
||||
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
||||
Some(operator) => {
|
||||
// Can't do anything for text objects, Ignoring
|
||||
error!("Unexpected normal mode motion operator: {:?}", operator)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn normal_object(object: Object, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
match vim.maybe_pop_operator() {
|
||||
Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
|
||||
Some(Operator::Change) => change_object(vim, object, around, cx),
|
||||
Some(Operator::Delete) => delete_object(vim, object, around, cx),
|
||||
Some(Operator::Yank) => yank_object(vim, object, around, cx),
|
||||
_ => {
|
||||
// Can't do anything for namespace operators. Ignoring
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
// Can't do anything with change/delete/yank and text objects. Ignoring
|
||||
}
|
||||
}
|
||||
vim.clear_operator(cx);
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn move_cursor(
|
||||
vim: &mut Vim,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
motion
|
||||
.move_point(map, cursor, goal, times, &text_layout_details)
|
||||
.unwrap_or((cursor, goal))
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
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(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
(
|
||||
first_non_whitespace(map, false, cursor),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
(next_line_end(map, cursor, 1), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (map, old_selections) = editor.selections.all_display(cx);
|
||||
let selection_start_rows: HashSet<u32> = old_selections
|
||||
.into_iter()
|
||||
.map(|selection| selection.start.row())
|
||||
.collect();
|
||||
let edits = selection_start_rows.into_iter().map(|row| {
|
||||
let (indent, _) = map.line_indent(row);
|
||||
let start_of_line =
|
||||
motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
|
||||
.to_point(&map);
|
||||
let mut new_text = " ".repeat(indent as usize);
|
||||
new_text.push('\n');
|
||||
(start_of_line..start_of_line, new_text)
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
|
||||
let insert_point = motion::end_of_line(map, false, previous_line);
|
||||
(insert_point, SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (map, old_selections) = editor.selections.all_display(cx);
|
||||
|
||||
let selection_end_rows: HashSet<u32> = old_selections
|
||||
.into_iter()
|
||||
.map(|selection| selection.end.row())
|
||||
.collect();
|
||||
let edits = selection_end_rows.into_iter().map(|row| {
|
||||
let (indent, _) = map.line_indent(row);
|
||||
let end_of_line =
|
||||
motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
|
||||
|
||||
let mut new_text = "\n".to_string();
|
||||
new_text.push_str(&" ".repeat(indent as usize));
|
||||
(end_of_line..end_of_line, new_text)
|
||||
});
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::CurrentLine.move_point(
|
||||
map,
|
||||
cursor,
|
||||
goal,
|
||||
None,
|
||||
&text_layout_details,
|
||||
)
|
||||
});
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.take_count(cx);
|
||||
yank_motion(vim, motion::Motion::CurrentLine, count, cx)
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
let (map, display_selections) = editor.selections.all_display(cx);
|
||||
// Selections are biased right at the start. So we need to store
|
||||
// anchors that are biased left so that we can restore the selections
|
||||
// after the change
|
||||
let stable_anchors = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let start = selection.start.bias_left(&map.buffer_snapshot);
|
||||
start..start
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let edits = display_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let mut range = selection.range();
|
||||
*range.end.column_mut() += 1;
|
||||
range.end = map.clip_point(range.end, Bias::Right);
|
||||
|
||||
(
|
||||
range.start.to_offset(&map, Bias::Left)
|
||||
..range.end.to_offset(&map, Bias::Left),
|
||||
text.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_anchor_ranges(stable_anchors);
|
||||
});
|
||||
});
|
||||
});
|
||||
vim.pop_operator(cx)
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
state::Mode::{self},
|
||||
test::NeovimBackedTestContext,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_h(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuick
|
||||
ˇbrown"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["backspace"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuick
|
||||
ˇbrown"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
aaˇaa
|
||||
😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
aaaa
|
||||
😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
for marked_position in cx.each_marked_position(indoc! {"
|
||||
ˇThe qˇuick broˇwn
|
||||
ˇfox jumps"
|
||||
}) {
|
||||
cx.assert_neovim_compatible(&marked_position, ["j"]).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuick broˇwn
|
||||
ˇfox jumps"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_k(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuick
|
||||
ˇbrown fˇox jumˇps"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_l(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuicˇk
|
||||
ˇbrowˇn"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_binding_matches_all(
|
||||
["$"],
|
||||
indoc! {"
|
||||
ˇThe qˇuicˇk
|
||||
ˇbrowˇn"},
|
||||
)
|
||||
.await;
|
||||
cx.assert_binding_matches_all(
|
||||
["0"],
|
||||
indoc! {"
|
||||
ˇThe qˇuicˇk
|
||||
ˇbrowˇn"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
|
||||
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquick
|
||||
|
||||
brown fox jumps
|
||||
overˇ the lazy doˇg"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quiˇck
|
||||
|
||||
brown"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quiˇck
|
||||
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_w(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquickˇ-ˇbrown
|
||||
ˇ
|
||||
ˇ
|
||||
ˇfox_jumps ˇover
|
||||
ˇthˇe"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-w"]);
|
||||
cx.assert_all(indoc! {"
|
||||
The ˇquickˇ-ˇbrown
|
||||
ˇ
|
||||
ˇ
|
||||
ˇfox_jumps ˇover
|
||||
ˇthˇe"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
|
||||
cx.assert_all(indoc! {"
|
||||
Thˇe quicˇkˇ-browˇn
|
||||
|
||||
|
||||
fox_jumpˇs oveˇr
|
||||
thˇe"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-e"]);
|
||||
cx.assert_all(indoc! {"
|
||||
Thˇe quicˇkˇ-browˇn
|
||||
|
||||
|
||||
fox_jumpˇs oveˇr
|
||||
thˇe"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe ˇquickˇ-ˇbrown
|
||||
ˇ
|
||||
ˇ
|
||||
ˇfox_jumps ˇover
|
||||
ˇthe"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-b"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe ˇquickˇ-ˇbrown
|
||||
ˇ
|
||||
ˇ
|
||||
ˇfox_jumps ˇover
|
||||
ˇthe"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gg(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_binding_matches_all(
|
||||
["g", "g"],
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
|
||||
brown fox jumps
|
||||
over ˇthe laˇzy dog"},
|
||||
)
|
||||
.await;
|
||||
cx.assert_binding_matches(
|
||||
["g", "g"],
|
||||
indoc! {"
|
||||
|
||||
|
||||
brown fox jumps
|
||||
over the laˇzy dog"},
|
||||
)
|
||||
.await;
|
||||
cx.assert_binding_matches(
|
||||
["2", "g", "g"],
|
||||
indoc! {"
|
||||
ˇ
|
||||
|
||||
brown fox jumps
|
||||
over the lazydog"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_binding_matches_all(
|
||||
["shift-g"],
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
|
||||
brown fox jumps
|
||||
over ˇthe laˇzy dog"},
|
||||
)
|
||||
.await;
|
||||
cx.assert_binding_matches(
|
||||
["shift-g"],
|
||||
indoc! {"
|
||||
|
||||
|
||||
brown fox jumps
|
||||
over the laˇzy dog"},
|
||||
)
|
||||
.await;
|
||||
cx.assert_binding_matches(
|
||||
["2", "shift-g"],
|
||||
indoc! {"
|
||||
ˇ
|
||||
|
||||
brown fox jumps
|
||||
over the lazydog"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_a(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
|
||||
cx.assert_all("The qˇuicˇk").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇ
|
||||
The qˇuick
|
||||
brown ˇfox "})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
|
||||
cx.assert("The qˇuick").await;
|
||||
cx.assert(" The qˇuick").await;
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
ˇ
|
||||
The quick"})
|
||||
.await;
|
||||
// Indoc disallows trailing whitespace.
|
||||
cx.assert(" ˇ \nThe quick").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
|
||||
cx.assert("The qˇuick").await;
|
||||
cx.assert(" The qˇuick").await;
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
ˇ
|
||||
The quick"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_x(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
|
||||
cx.assert_all("ˇTeˇsˇt").await;
|
||||
cx.assert(indoc! {"
|
||||
Tesˇt
|
||||
test"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_left(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
|
||||
cx.assert_all("ˇTˇeˇsˇt").await;
|
||||
cx.assert(indoc! {"
|
||||
Test
|
||||
ˇtest"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_o(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The qˇuick
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
|
||||
cx.assert_manual(
|
||||
indoc! {"
|
||||
fn test() {
|
||||
println!(ˇ);
|
||||
}"},
|
||||
Mode::Normal,
|
||||
indoc! {"
|
||||
fn test() {
|
||||
println!();
|
||||
ˇ
|
||||
}"},
|
||||
Mode::Insert,
|
||||
);
|
||||
|
||||
cx.assert_manual(
|
||||
indoc! {"
|
||||
fn test(ˇ) {
|
||||
println!();
|
||||
}"},
|
||||
Mode::Normal,
|
||||
indoc! {"
|
||||
fn test() {
|
||||
ˇ
|
||||
println!();
|
||||
}"},
|
||||
Mode::Insert,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
|
||||
let cx = NeovimBackedTestContext::new(cx).await;
|
||||
let mut cx = cx.binding(["shift-o"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The qˇuick
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
|
||||
// Our indentation is smarter than vims. So we don't match here
|
||||
cx.assert_manual(
|
||||
indoc! {"
|
||||
fn test() {
|
||||
println!(ˇ);
|
||||
}"},
|
||||
Mode::Normal,
|
||||
indoc! {"
|
||||
fn test() {
|
||||
ˇ
|
||||
println!();
|
||||
}"},
|
||||
Mode::Insert,
|
||||
);
|
||||
cx.assert_manual(
|
||||
indoc! {"
|
||||
fn test(ˇ) {
|
||||
println!();
|
||||
}"},
|
||||
Mode::Normal,
|
||||
indoc! {"
|
||||
ˇ
|
||||
fn test() {
|
||||
println!();
|
||||
}"},
|
||||
Mode::Insert,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dd(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
|
||||
cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
|
||||
for marked_text in cx.each_marked_position(indoc! {"
|
||||
The qˇuick
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
{
|
||||
cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
|
||||
}
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
["d", "d"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cc(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The quˇick
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
[&count.to_string(), "w"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
||||
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=3 {
|
||||
let test_case = indoc! {"
|
||||
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
|
||||
ˇ ˇbˇaaˇa ˇbˇbˇb
|
||||
ˇ
|
||||
ˇb
|
||||
"};
|
||||
|
||||
cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
|
||||
.await;
|
||||
|
||||
cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
let test_case = indoc! {"
|
||||
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
|
||||
ˇ ˇbˇaaˇa ˇbˇbˇb
|
||||
ˇ•••
|
||||
ˇb
|
||||
"
|
||||
};
|
||||
|
||||
for count in 1..=3 {
|
||||
cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
|
||||
.await;
|
||||
|
||||
cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_percent(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
|
||||
cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
|
||||
cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
|
||||
.await;
|
||||
cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
use editor::scroll::autoscroll::Autoscroll;
|
||||
use gpui::ViewContext;
|
||||
use language::{Bias, Point};
|
||||
use workspace::Workspace;
|
||||
|
||||
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(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
let mut cursor_positions = Vec::new();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in editor.selections.all::<Point>(cx) {
|
||||
match vim.state().mode {
|
||||
Mode::VisualLine => {
|
||||
let start = Point::new(selection.start.row, 0);
|
||||
let end =
|
||||
Point::new(selection.end.row, snapshot.line_len(selection.end.row));
|
||||
ranges.push(start..end);
|
||||
cursor_positions.push(start..start);
|
||||
}
|
||||
Mode::Visual => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
Mode::VisualBlock => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
if cursor_positions.len() == 0 {
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
}
|
||||
Mode::Insert | Mode::Normal => {
|
||||
let start = selection.start;
|
||||
let mut end = start;
|
||||
for _ in 0..count {
|
||||
end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
|
||||
}
|
||||
ranges.push(start..end);
|
||||
|
||||
if end.column == snapshot.line_len(end.row) {
|
||||
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
|
||||
}
|
||||
cursor_positions.push(end..end)
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.transact(cx, |editor, cx| {
|
||||
for range in ranges.into_iter().rev() {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.flat_map(|s| s.chars())
|
||||
.flat_map(|c| {
|
||||
if c.is_lowercase() {
|
||||
c.to_uppercase().collect::<Vec<char>>()
|
||||
} else {
|
||||
c.to_lowercase().collect::<Vec<char>>()
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
buffer.edit([(range, text)], None, cx)
|
||||
})
|
||||
}
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(cursor_positions)
|
||||
})
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, true, cx)
|
||||
})
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::NeovimBackedTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇabC\n").await;
|
||||
cx.simulate_shared_keystrokes(["~"]).await;
|
||||
cx.assert_shared_state("AˇbC\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "~"]).await;
|
||||
cx.assert_shared_state("ABˇc\n").await;
|
||||
|
||||
// works in visual mode
|
||||
cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
|
||||
cx.simulate_shared_keystrokes(["~"]).await;
|
||||
cx.assert_shared_state("a😀CˇDé1*F\n").await;
|
||||
|
||||
// works with multibyte characters
|
||||
cx.simulate_shared_keystrokes(["~"]).await;
|
||||
cx.set_shared_state("aˇC😀é1*F\n").await;
|
||||
cx.simulate_shared_keystrokes(["4", "~"]).await;
|
||||
cx.assert_shared_state("ac😀É1ˇ*F\n").await;
|
||||
|
||||
// works with line selections
|
||||
cx.set_shared_state("abˇC\n").await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
|
||||
cx.assert_shared_state("ˇABc\n").await;
|
||||
|
||||
// works in visual block mode
|
||||
cx.set_shared_state("ˇaa\nbb\ncc").await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
|
||||
cx.assert_shared_state("ˇAa\nBb\ncc").await;
|
||||
|
||||
// works with multiple cursors (zed only)
|
||||
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
||||
cx.simulate_keystroke("~");
|
||||
cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
|
||||
}
|
||||
}
|
@ -1,502 +0,0 @@
|
||||
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
|
||||
use editor::{
|
||||
char_kind,
|
||||
display_map::DisplaySnapshot,
|
||||
movement::{self, FindRange, TextLayoutDetails},
|
||||
scroll::autoscroll::Autoscroll,
|
||||
CharKind, DisplayPoint,
|
||||
};
|
||||
use gpui::WindowContext;
|
||||
use language::Selection;
|
||||
|
||||
pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
// Some motions ignore failure when switching to normal mode
|
||||
let mut motion_succeeded = matches!(
|
||||
motion,
|
||||
Motion::Left
|
||||
| Motion::Right
|
||||
| Motion::EndOfLine { .. }
|
||||
| Motion::Backspace
|
||||
| Motion::StartOfLine { .. }
|
||||
);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
|
||||
{
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
)
|
||||
} else {
|
||||
motion.expand_selection(map, selection, times, false, &text_layout_details)
|
||||
};
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
editor.insert("", cx);
|
||||
});
|
||||
});
|
||||
|
||||
if motion_succeeded {
|
||||
vim.switch_mode(Mode::Insert, false, cx)
|
||||
} else {
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
let mut objects_found = false;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
objects_found |= object.expand_selection(map, selection, around);
|
||||
});
|
||||
});
|
||||
if objects_found {
|
||||
copy_selections_content(editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if objects_found {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
} else {
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
|
||||
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
|
||||
// on a non-blank. This is because "cw" is interpreted as change-word, and a
|
||||
// word does not include the following white space. {Vi: "cw" when on a blank
|
||||
// followed by other blanks changes only the first blank; this is probably a
|
||||
// bug, because "dw" deletes all the blanks}
|
||||
fn expand_changed_word_selection(
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: Option<usize>,
|
||||
ignore_punctuation: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> bool {
|
||||
if times.is_none() || times.unwrap() == 1 {
|
||||
let scope = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(selection.start.to_point(map));
|
||||
let in_word = map
|
||||
.chars_at(selection.head())
|
||||
.next()
|
||||
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
|
||||
.unwrap_or_default();
|
||||
|
||||
if in_word {
|
||||
selection.end =
|
||||
movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
|
||||
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind =
|
||||
char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||
});
|
||||
true
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tˇest").await;
|
||||
cx.assert("ˇTest").await;
|
||||
cx.assert(indoc! {"
|
||||
Test
|
||||
ˇtest"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["c", "backspace"]);
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tˇest").await;
|
||||
cx.assert("ˇTest").await;
|
||||
cx.assert(indoc! {"
|
||||
Test
|
||||
ˇtest"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_l(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tesˇt").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_w(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tˇest test").await;
|
||||
cx.assert("Testˇ test").await;
|
||||
cx.assert(indoc! {"
|
||||
Test teˇst
|
||||
test"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test tesˇt
|
||||
test"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"})
|
||||
.await;
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-w"]);
|
||||
cx.assert("Test teˇst-test test").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_e(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
|
||||
cx.assert("Teˇst Test").await;
|
||||
cx.assert("Tˇest test").await;
|
||||
cx.assert(indoc! {"
|
||||
Test teˇst
|
||||
test"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test tesˇt
|
||||
test"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"})
|
||||
.await;
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-e"]);
|
||||
cx.assert("Test teˇst-test test").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
|
||||
cx.assert("Teˇst Test").await;
|
||||
cx.assert("Test ˇtest").await;
|
||||
cx.assert("Test1 test2 ˇtest3").await;
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇtest"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"})
|
||||
.await;
|
||||
|
||||
let mut cx = cx.binding(["c", "shift-b"]);
|
||||
cx.assert("Test test-test ˇtest").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
["c", "0"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
["c", "0"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over"},
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇ"},
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "j"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "l"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
for marked_text in cx.each_marked_position(indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"})
|
||||
{
|
||||
cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "e"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,475 +0,0 @@
|
||||
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::WindowContext;
|
||||
use language::Point;
|
||||
|
||||
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| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_head = selection.head();
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
motion.expand_selection(map, selection, times, true, &text_layout_details);
|
||||
|
||||
// Motion::NextWordStart on an empty line should delete it.
|
||||
if let Motion::NextWordStart {
|
||||
ignore_punctuation: _,
|
||||
} = motion
|
||||
{
|
||||
if selection.is_empty()
|
||||
&& map
|
||||
.buffer_snapshot
|
||||
.line_len(selection.start.to_point(&map).row)
|
||||
== 0
|
||||
{
|
||||
selection.end = map
|
||||
.buffer_snapshot
|
||||
.clip_point(
|
||||
Point::new(selection.start.to_point(&map).row + 1, 0),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_display_point(map)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
editor.insert("", cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if motion.linewise() {
|
||||
if let Some(column) = original_columns.get(&selection.id) {
|
||||
*cursor.column_mut() = *column
|
||||
}
|
||||
}
|
||||
cursor = map.clip_point(cursor, Bias::Left);
|
||||
selection.collapse_to(cursor, selection.goal)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
// Emulates behavior in vim where if we expanded backwards to include a newline
|
||||
// the cursor gets set back to the start of the line
|
||||
let mut should_move_to_start: HashSet<_> = Default::default();
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
|
||||
let contains_only_newlines = map
|
||||
.chars_at(selection.start)
|
||||
.take_while(|(_, p)| p < &selection.end)
|
||||
.all(|(char, _)| char == '\n')
|
||||
&& !offset_range.is_empty();
|
||||
let end_at_newline = map
|
||||
.chars_at(selection.end)
|
||||
.next()
|
||||
.map(|(c, _)| c == '\n')
|
||||
.unwrap_or(false);
|
||||
|
||||
// If expanded range contains only newlines and
|
||||
// the object is around or sentence, expand to include a newline
|
||||
// at the end or start
|
||||
if (around || object == Object::Sentence) && contains_only_newlines {
|
||||
if end_at_newline {
|
||||
selection.end =
|
||||
(offset_range.end + '\n'.len_utf8()).to_display_point(map);
|
||||
} else if selection.start.row() > 0 {
|
||||
should_move_to_start.insert(selection.id);
|
||||
selection.start =
|
||||
(offset_range.start - '\n'.len_utf8()).to_display_point(map);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if should_move_to_start.contains(&selection.id) {
|
||||
*cursor.column_mut() = 0;
|
||||
}
|
||||
cursor = map.clip_point(cursor, Bias::Left);
|
||||
selection.collapse_to(cursor, selection.goal)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "h"]);
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tˇest").await;
|
||||
cx.assert("ˇTest").await;
|
||||
cx.assert(indoc! {"
|
||||
Test
|
||||
ˇtest"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_l(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "l"]);
|
||||
cx.assert("ˇTest").await;
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tesˇt").await;
|
||||
cx.assert(indoc! {"
|
||||
Tesˇt
|
||||
test"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_w(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
["d", "w"],
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.assert_neovim_compatible("Teˇst", ["d", "w"]).await;
|
||||
cx.assert_neovim_compatible("Tˇest test", ["d", "w"]).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
Test teˇst
|
||||
test"},
|
||||
["d", "w"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
["d", "w"],
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
["d", "w"],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-w"]);
|
||||
cx.assert_neovim_compatible("Test teˇst-test test", ["d", "shift-w"])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
|
||||
// cx.assert("Teˇst Test").await;
|
||||
// cx.assert("Tˇest test").await;
|
||||
cx.assert(indoc! {"
|
||||
Test teˇst
|
||||
test"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test tesˇt
|
||||
test"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
ExemptionFeatures::OperatorLastNewlineRemains,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-e"]);
|
||||
cx.assert("Test teˇst-test test").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "b"]);
|
||||
cx.assert("Teˇst Test").await;
|
||||
cx.assert("Test ˇtest").await;
|
||||
cx.assert("Test1 test2 ˇtest3").await;
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇtest"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"})
|
||||
.await;
|
||||
|
||||
let mut cx = cx.binding(["d", "shift-b"]);
|
||||
cx.assert("Test test-test ˇtest").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "$"]);
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_0(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "0"]);
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_k(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "k"]);
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
ˇbrown fox
|
||||
jumps over"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "j"]);
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇ"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["d", "g", "g"]);
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
// Canceling operator twice reverts to normal mode with no active operator
|
||||
cx.simulate_keystrokes(["d", "escape", "k"]);
|
||||
assert_eq!(cx.active_operator(), None);
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
// Canceling operator twice reverts to normal mode with no active operator
|
||||
cx.simulate_keystrokes(["d", "y"]);
|
||||
assert_eq!(cx.active_operator(), None);
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the moon,
|
||||
a star, and
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
}
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use editor::{scroll::autoscroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use gpui::{impl_actions, ViewContext, WindowContext};
|
||||
use language::{Bias, Point};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{state::Mode, Vim};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Increment {
|
||||
#[serde(default)]
|
||||
step: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Decrement {
|
||||
#[serde(default)]
|
||||
step: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [Increment, Decrement]);
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, action: &Increment, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
let step = if action.step { 1 } else { 0 };
|
||||
increment(vim, count as i32, step, cx)
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, action: &Decrement, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
let step = if action.step { -1 } else { 0 };
|
||||
increment(vim, count as i32 * -1, step, cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut edits = Vec::new();
|
||||
let mut new_anchors = Vec::new();
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in editor.selections.all_adjusted(cx) {
|
||||
if !selection.is_empty() {
|
||||
if vim.state().mode != Mode::VisualBlock || new_anchors.is_empty() {
|
||||
new_anchors.push((true, snapshot.anchor_before(selection.start)))
|
||||
}
|
||||
}
|
||||
for row in selection.start.row..=selection.end.row {
|
||||
let start = if row == selection.start.row {
|
||||
selection.start
|
||||
} else {
|
||||
Point::new(row, 0)
|
||||
};
|
||||
|
||||
if let Some((range, num, radix)) = find_number(&snapshot, start) {
|
||||
if let Ok(val) = i32::from_str_radix(&num, radix) {
|
||||
let result = val + delta;
|
||||
delta += step;
|
||||
let replace = match radix {
|
||||
10 => format!("{}", result),
|
||||
16 => {
|
||||
if num.to_ascii_lowercase() == num {
|
||||
format!("{:x}", result)
|
||||
} else {
|
||||
format!("{:X}", result)
|
||||
}
|
||||
}
|
||||
2 => format!("{:b}", result),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
edits.push((range.clone(), replace));
|
||||
}
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||
}
|
||||
} else {
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((true, snapshot.anchor_after(start)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.edit(edits, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let mut new_ranges = Vec::new();
|
||||
for (visual, anchor) in new_anchors.iter() {
|
||||
let mut point = anchor.to_point(&snapshot);
|
||||
if !*visual && point.column > 0 {
|
||||
point.column -= 1;
|
||||
point = snapshot.clip_point(point, Bias::Left)
|
||||
}
|
||||
new_ranges.push(point..point);
|
||||
}
|
||||
s.select_ranges(new_ranges)
|
||||
})
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, true, cx)
|
||||
}
|
||||
|
||||
fn find_number(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
start: Point,
|
||||
) -> Option<(Range<Point>, String, u32)> {
|
||||
let mut offset = start.to_offset(snapshot);
|
||||
|
||||
// go backwards to the start of any number the selection is within
|
||||
for ch in snapshot.reversed_chars_at(offset) {
|
||||
if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
|
||||
offset -= ch.len_utf8();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let mut begin = None;
|
||||
let mut end = None;
|
||||
let mut num = String::new();
|
||||
let mut radix = 10;
|
||||
|
||||
let mut chars = snapshot.chars_at(offset).peekable();
|
||||
// find the next number on the line (may start after the original cursor position)
|
||||
while let Some(ch) = chars.next() {
|
||||
if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
|
||||
radix = 2;
|
||||
begin = None;
|
||||
num = String::new();
|
||||
}
|
||||
if num == "0" && ch == 'x' && chars.peek().is_some() && chars.peek().unwrap().is_digit(16) {
|
||||
radix = 16;
|
||||
begin = None;
|
||||
num = String::new();
|
||||
}
|
||||
|
||||
if ch.is_digit(radix)
|
||||
|| (begin.is_none()
|
||||
&& ch == '-'
|
||||
&& chars.peek().is_some()
|
||||
&& chars.peek().unwrap().is_digit(radix))
|
||||
{
|
||||
if begin.is_none() {
|
||||
begin = Some(offset);
|
||||
}
|
||||
num.push(ch);
|
||||
} else {
|
||||
if begin.is_some() {
|
||||
end = Some(offset);
|
||||
break;
|
||||
} else if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
if let Some(begin) = begin {
|
||||
let end = end.unwrap_or(offset);
|
||||
Some((begin.to_point(snapshot)..end.to_point(snapshot), num, radix))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
1ˇ2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["ctrl-a"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
1ˇ3
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-x"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
1ˇ2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["9", "9", "ctrl-a"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
11ˇ1
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["1", "1", "1", "ctrl-x"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇ0
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
-11ˇ1
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_matches_neovim("ˇ total: 0xff", ["ctrl-a"], " total: 0x10ˇ0")
|
||||
.await;
|
||||
cx.assert_matches_neovim("ˇ total: 0xff", ["ctrl-x"], " total: 0xfˇe")
|
||||
.await;
|
||||
cx.assert_matches_neovim("ˇ total: 0xFF", ["ctrl-x"], " total: 0xFˇE")
|
||||
.await;
|
||||
cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)")
|
||||
.await;
|
||||
cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await;
|
||||
cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana")
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇ1
|
||||
1
|
||||
1 2
|
||||
1
|
||||
1"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["j", "v", "shift-g", "g", "ctrl-a"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
1
|
||||
ˇ2
|
||||
3 2
|
||||
4
|
||||
5"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
«1ˇ»
|
||||
«2ˇ»
|
||||
«3ˇ» 2
|
||||
«4ˇ»
|
||||
«5ˇ»"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇ0
|
||||
0
|
||||
0 2
|
||||
0
|
||||
0"})
|
||||
.await;
|
||||
}
|
||||
}
|
@ -1,476 +0,0 @@
|
||||
use std::{borrow::Cow, cmp};
|
||||
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::{impl_actions, ViewContext};
|
||||
use language::{Bias, SelectionGoal};
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{state::Mode, utils::copy_selections_content, Vim};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Paste {
|
||||
#[serde(default)]
|
||||
before: bool,
|
||||
#[serde(default)]
|
||||
preserve_clipboard: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [Paste]);
|
||||
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(paste);
|
||||
}
|
||||
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let Some(item) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
let clipboard_text = Cow::Borrowed(item.text());
|
||||
if clipboard_text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !action.preserve_clipboard && vim.state().mode.is_visual() {
|
||||
copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
|
||||
}
|
||||
|
||||
// if we are copying from multi-cursor (of visual block mode), we want
|
||||
// to
|
||||
let clipboard_selections =
|
||||
item.metadata::<Vec<ClipboardSelection>>()
|
||||
.filter(|clipboard_selections| {
|
||||
clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
|
||||
});
|
||||
|
||||
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
|
||||
|
||||
// unlike zed, if you have a multi-cursor selection from vim block mode,
|
||||
// pasting it will paste it on subsequent lines, even if you don't yet
|
||||
// have a cursor there.
|
||||
let mut selections_to_process = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < current_selections.len() {
|
||||
selections_to_process
|
||||
.push((current_selections[i].start..current_selections[i].end, true));
|
||||
i += 1;
|
||||
}
|
||||
if let Some(clipboard_selections) = clipboard_selections.as_ref() {
|
||||
let left = current_selections
|
||||
.iter()
|
||||
.map(|selection| cmp::min(selection.start.column(), selection.end.column()))
|
||||
.min()
|
||||
.unwrap();
|
||||
let mut row = current_selections.last().unwrap().end.row() + 1;
|
||||
while i < clipboard_selections.len() {
|
||||
let cursor =
|
||||
display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
|
||||
selections_to_process.push((cursor..cursor, false));
|
||||
i += 1;
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let first_selection_indent_column =
|
||||
clipboard_selections.as_ref().and_then(|zed_selections| {
|
||||
zed_selections
|
||||
.first()
|
||||
.map(|selection| selection.first_line_indent)
|
||||
});
|
||||
let before = action.before || vim.state().mode == Mode::VisualLine;
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut new_selections = Vec::new();
|
||||
let mut original_indent_columns = Vec::new();
|
||||
let mut start_offset = 0;
|
||||
|
||||
for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
|
||||
let (mut to_insert, original_indent_column) =
|
||||
if let Some(clipboard_selections) = &clipboard_selections {
|
||||
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
|
||||
let end_offset = start_offset + clipboard_selection.len;
|
||||
let text = clipboard_text[start_offset..end_offset].to_string();
|
||||
start_offset = end_offset + 1;
|
||||
(text, Some(clipboard_selection.first_line_indent))
|
||||
} else {
|
||||
("".to_string(), first_selection_indent_column)
|
||||
}
|
||||
} else {
|
||||
(clipboard_text.to_string(), first_selection_indent_column)
|
||||
};
|
||||
let line_mode = to_insert.ends_with("\n");
|
||||
let is_multiline = to_insert.contains("\n");
|
||||
|
||||
if line_mode && !before {
|
||||
if selection.is_empty() {
|
||||
to_insert =
|
||||
"\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
|
||||
} else {
|
||||
to_insert = "\n".to_owned() + &to_insert;
|
||||
}
|
||||
} else if !line_mode && vim.state().mode == Mode::VisualLine {
|
||||
to_insert = to_insert + "\n";
|
||||
}
|
||||
|
||||
let display_range = if !selection.is_empty() {
|
||||
selection.start..selection.end
|
||||
} else if line_mode {
|
||||
let point = if before {
|
||||
movement::line_beginning(&display_map, selection.start, false)
|
||||
} else {
|
||||
movement::line_end(&display_map, selection.start, false)
|
||||
};
|
||||
point..point
|
||||
} else {
|
||||
let point = if before {
|
||||
selection.start
|
||||
} else {
|
||||
movement::saturating_right(&display_map, selection.start)
|
||||
};
|
||||
point..point
|
||||
};
|
||||
|
||||
let point_range = display_range.start.to_point(&display_map)
|
||||
..display_range.end.to_point(&display_map);
|
||||
let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
|
||||
display_map.buffer_snapshot.anchor_before(point_range.start)
|
||||
} else {
|
||||
display_map.buffer_snapshot.anchor_after(point_range.end)
|
||||
};
|
||||
|
||||
if *preserve {
|
||||
new_selections.push((anchor, line_mode, is_multiline));
|
||||
}
|
||||
edits.push((point_range, to_insert));
|
||||
original_indent_columns.extend(original_indent_column);
|
||||
}
|
||||
|
||||
editor.edit_with_block_indent(edits, original_indent_columns, cx);
|
||||
|
||||
// in line_mode vim will insert the new text on the next (or previous if before) line
|
||||
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
|
||||
// otherwise vim will insert the next text at (or before) the current cursor position,
|
||||
// the cursor will go to the last (or first, if is_multiline) inserted character.
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.replace_cursors_with(|map| {
|
||||
let mut cursors = Vec::new();
|
||||
for (anchor, line_mode, is_multiline) in &new_selections {
|
||||
let mut cursor = anchor.to_display_point(map);
|
||||
if *line_mode {
|
||||
if !before {
|
||||
cursor = movement::down(
|
||||
map,
|
||||
cursor,
|
||||
SelectionGoal::None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
.0;
|
||||
}
|
||||
cursor = movement::indented_line_beginning(map, cursor, true);
|
||||
} else if !is_multiline {
|
||||
cursor = movement::saturating_left(map, cursor)
|
||||
}
|
||||
cursors.push(cursor);
|
||||
if vim.state().mode == Mode::VisualBlock {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cursors
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// single line
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox ˇjumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
|
||||
cx.assert_shared_clipboard("jumps o").await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps oveˇr
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps overjumps ˇo
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps oveˇr
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystroke("shift-p").await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps ovejumps ˇor
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
// line mode
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["d", "d"]).await;
|
||||
cx.assert_shared_clipboard("fox jumps over\n").await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
the lazy dog
|
||||
ˇfox jumps over"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
ˇfox jumps over
|
||||
the lazy dog
|
||||
fox jumps over"})
|
||||
.await;
|
||||
|
||||
// multiline, cursor to first character of pasted text.
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
|
||||
cx.assert_shared_clipboard("over\nthe lazy do").await;
|
||||
|
||||
cx.simulate_shared_keystroke("p").await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps oˇover
|
||||
the lazy dover
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps ˇover
|
||||
the lazy doover
|
||||
the lazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// copy in visual mode
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jˇumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox ˇjumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// paste in visual mode
|
||||
cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox jumps jumpˇs
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.assert_shared_clipboard("over").await;
|
||||
// paste in visual line mode
|
||||
cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇover
|
||||
fox jumps jumps
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.assert_shared_clipboard("over").await;
|
||||
// paste in visual block mode
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
oveˇrver
|
||||
overox jumps jumps
|
||||
overhe lazy dog"})
|
||||
.await;
|
||||
|
||||
// copy in visual line mode
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
// paste in visual mode
|
||||
cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
|
||||
cx.assert_shared_state(
|
||||
&indoc! {"
|
||||
The quick brown
|
||||
the_
|
||||
ˇfox jumps over
|
||||
_dog"}
|
||||
.replace("_", " "), // Hack for trailing whitespace
|
||||
)
|
||||
.await;
|
||||
cx.assert_shared_clipboard("lazy").await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
// paste in visual line mode
|
||||
cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇfox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.assert_shared_clipboard("The quick brown\n").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
// copy in visual block mode
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
|
||||
.await;
|
||||
cx.assert_shared_clipboard("q\nj\nl").await;
|
||||
cx.simulate_shared_keystrokes(["p"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The qˇquick brown
|
||||
fox jjumps over
|
||||
the llazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The ˇq brown
|
||||
fox jjjumps over
|
||||
the lllazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
|
||||
cx.assert_shared_clipboard("q\nj").await;
|
||||
cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The qˇqick brown
|
||||
fox jjmps over
|
||||
the lzy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇq
|
||||
j
|
||||
fox jjmps over
|
||||
the lzy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new_typescript(cx).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
class A {ˇ
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
class A {
|
||||
a()ˇ{}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
// cursor goes to the first non-blank character in the line;
|
||||
cx.simulate_keystrokes(["y", "y", "p"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
class A {
|
||||
a(){}
|
||||
ˇa(){}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
// indentation is preserved when pasting
|
||||
cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
ˇclass A {
|
||||
a(){}
|
||||
class A {
|
||||
a(){}
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,496 +0,0 @@
|
||||
use crate::{
|
||||
insert::NormalBefore,
|
||||
motion::Motion,
|
||||
state::{Mode, RecordedSelection, ReplayableAction},
|
||||
visual::visual_motion,
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, Action, ViewContext, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat]);
|
||||
|
||||
fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
// skip so that we don't leave the character palette open
|
||||
if editor::ShowCharacterPalette.partial_eq(&**action) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if super::InsertBefore.partial_eq(&**action)
|
||||
|| super::InsertAfter.partial_eq(&**action)
|
||||
|| super::InsertFirstNonWhitespace.partial_eq(&**action)
|
||||
|| super::InsertEndOfLine.partial_eq(&**action)
|
||||
{
|
||||
Some(super::InsertBefore.boxed_clone())
|
||||
} else if super::InsertLineAbove.partial_eq(&**action)
|
||||
|| super::InsertLineBelow.partial_eq(&**action)
|
||||
{
|
||||
Some(super::InsertLineBelow.boxed_clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.replaying = false;
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
});
|
||||
});
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
|
||||
}
|
||||
|
||||
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
if actions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.take_count(cx);
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { cols } => {
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::Visual { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
visual_motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualBlock { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualLine { rows } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RecordedSelection::None => {}
|
||||
}
|
||||
|
||||
// insert internally uses repeat to handle counts
|
||||
// vim doesn't treat 3a1 as though you literally repeated a1
|
||||
// 3 times, instead it inserts the content thrice at the insert position.
|
||||
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
|
||||
if let Some(ReplayableAction::Action(action)) = actions.last() {
|
||||
if NormalBefore.partial_eq(&**action) {
|
||||
actions.pop();
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_actions = actions.clone();
|
||||
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
|
||||
|
||||
let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
|
||||
|
||||
// if we came from insert mode we're just doing repititions 2 onwards.
|
||||
if from_insert_mode {
|
||||
count -= 1;
|
||||
new_actions[0] = actions[0].clone();
|
||||
}
|
||||
|
||||
for _ in 1..count {
|
||||
new_actions.append(actions.clone().as_mut());
|
||||
}
|
||||
new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
|
||||
actions = new_actions;
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
|
||||
let window = cx.window_handle();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})?;
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window.update(&mut cx, |_, cx| cx.dispatch_action(action))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => editor.update(&mut cx, |editor, cx| {
|
||||
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
|
||||
}),
|
||||
}?
|
||||
}
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.show_local_selections = true;
|
||||
})?;
|
||||
window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone()))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
|
||||
use gpui::InputHandler;
|
||||
|
||||
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.run_until_parked();
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.assert_shared_state("THE QUICK ˇbrown fox").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_ime(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(["."]);
|
||||
cx.assert_state("hˇäällo", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
|
||||
VimTestContext::init(cx);
|
||||
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", "."]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.second!
|
||||
two.secondˇ!
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// single-line (3 columns)
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇo quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "w", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o quick brown
|
||||
fox ˇops over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["f", "r", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o quick brown
|
||||
fox ops oveˇothe lazy dog"
|
||||
})
|
||||
.await;
|
||||
|
||||
// visual
|
||||
cx.set_shared_state(indoc! {
|
||||
"the ˇquick brown
|
||||
fox jumps over
|
||||
fox jumps over
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the ˇumps over
|
||||
fox jumps over
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the ˇumps over
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["w", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the umps ˇumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the umps umps over
|
||||
the ˇog"
|
||||
})
|
||||
.await;
|
||||
|
||||
// block mode (3 rows)
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇothe quick brown
|
||||
ofox jumps over
|
||||
othe lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"othe quick brown
|
||||
ofoxˇo jumps over
|
||||
otheo lazy dog"
|
||||
})
|
||||
.await;
|
||||
|
||||
// line mode
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇo
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o
|
||||
ˇo
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇ brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
" brown
|
||||
ˇ over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "2", "."]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
" brown
|
||||
over
|
||||
ˇe lazy dog"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape"]);
|
||||
cx.simulate_keystrokes(["escape"]);
|
||||
cx.assert_state("ˇjhello\n", Mode::Normal);
|
||||
}
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
use crate::Vim;
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint,
|
||||
scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN},
|
||||
DisplayPoint, Editor,
|
||||
};
|
||||
use gpui::{actions, ViewContext};
|
||||
use language::Bias;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown]
|
||||
);
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, _: &LineDown, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &LineUp, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &PageDown, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &PageUp, cx| {
|
||||
scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &ScrollDown, cx| {
|
||||
scroll(cx, true, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(c)
|
||||
} else {
|
||||
ScrollAmount::Page(0.5)
|
||||
}
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_: &mut Workspace, _: &ScrollUp, cx| {
|
||||
scroll(cx, true, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(-c)
|
||||
} else {
|
||||
ScrollAmount::Page(-0.5)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn scroll(
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
move_cursor: bool,
|
||||
by: fn(c: Option<f32>) -> ScrollAmount,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let amount = by(vim.take_count(cx).map(|c| c as f32));
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
scroll_editor(editor, move_cursor, &amount, cx)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn scroll_editor(
|
||||
editor: &mut Editor,
|
||||
preserve_cursor_position: bool,
|
||||
amount: &ScrollAmount,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
|
||||
let old_top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
|
||||
editor.scroll_screen(amount, cx);
|
||||
if should_move_cursor {
|
||||
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
|
||||
visible_rows as u32
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut head = selection.head();
|
||||
let top = top_anchor.to_display_point(map);
|
||||
|
||||
if preserve_cursor_position {
|
||||
let old_top = old_top_anchor.to_display_point(map);
|
||||
let new_row = top.row() + selection.head().row() - old_top.row();
|
||||
head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
|
||||
}
|
||||
let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
|
||||
let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
|
||||
|
||||
let new_head = if head.row() < min_row {
|
||||
map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left)
|
||||
} else if head.row() > max_row {
|
||||
map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left)
|
||||
} else {
|
||||
head
|
||||
};
|
||||
if selection.is_empty() {
|
||||
selection.collapse_to(new_head, selection.goal)
|
||||
} else {
|
||||
selection.set_head(new_head, selection.goal)
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use gpui::{point, px, size, Context};
|
||||
use indoc::indoc;
|
||||
use language::Point;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let (line_height, visible_line_count) = cx.editor(|editor, cx| {
|
||||
(
|
||||
editor
|
||||
.style()
|
||||
.unwrap()
|
||||
.text
|
||||
.line_height_in_pixels(cx.rem_size()),
|
||||
editor.visible_line_count().unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = cx.window;
|
||||
let margin = cx
|
||||
.update_window(window, |_, cx| {
|
||||
cx.viewport_size().height - line_height * visible_line_count
|
||||
})
|
||||
.unwrap();
|
||||
cx.simulate_window_resize(
|
||||
cx.window,
|
||||
size(px(1000.), margin + 8. * line_height - px(1.0)),
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc!(
|
||||
"ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
"
|
||||
),
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-e"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 1.))
|
||||
});
|
||||
cx.simulate_keystrokes(["2", "ctrl-e"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-y"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 2.))
|
||||
});
|
||||
|
||||
// does not select in normal mode
|
||||
cx.simulate_keystrokes(["g", "g"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-d"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.0));
|
||||
assert_eq!(
|
||||
editor.selections.newest(cx).range(),
|
||||
Point::new(6, 0)..Point::new(6, 0)
|
||||
)
|
||||
});
|
||||
|
||||
// does select in visual mode
|
||||
cx.simulate_keystrokes(["g", "g"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["v", "ctrl-d"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.0));
|
||||
assert_eq!(
|
||||
editor.selections.newest(cx).range(),
|
||||
Point::new(0, 0)..Point::new(6, 1)
|
||||
)
|
||||
});
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(10).await;
|
||||
|
||||
pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
|
||||
let mut text = String::new();
|
||||
for row in 0..rows {
|
||||
let c: char = (start_char as u32 + row as u32) as u8 as char;
|
||||
let mut line = c.to_string().repeat(cols);
|
||||
if row < rows - 1 {
|
||||
line.push('\n');
|
||||
}
|
||||
text += &line;
|
||||
}
|
||||
text
|
||||
}
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
// skip over the scrolloff at the top
|
||||
// test ctrl-d
|
||||
cx.simulate_shared_keystrokes(["4", "j", "ctrl-d"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-d"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.simulate_shared_keystrokes(["g", "g", "ctrl-d"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
// test ctrl-u
|
||||
cx.simulate_shared_keystrokes(["ctrl-u"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-d", "ctrl-d", "4", "j", "ctrl-u", "ctrl-u"])
|
||||
.await;
|
||||
cx.assert_state_matches().await;
|
||||
}
|
||||
}
|
@ -1,477 +0,0 @@
|
||||
use gpui::{actions, impl_actions, ViewContext};
|
||||
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{searchable::Direction, Workspace};
|
||||
|
||||
use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MoveToNext {
|
||||
#[serde(default)]
|
||||
partial_word: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MoveToPrev {
|
||||
#[serde(default)]
|
||||
partial_word: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub(crate) struct Search {
|
||||
#[serde(default)]
|
||||
backwards: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct FindCommand {
|
||||
pub query: String,
|
||||
pub backwards: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct ReplaceCommand {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Replacement {
|
||||
search: String,
|
||||
replacement: String,
|
||||
should_replace_all: bool,
|
||||
is_case_sensitive: bool,
|
||||
}
|
||||
|
||||
actions!(vim, [SearchSubmit]);
|
||||
impl_actions!(
|
||||
vim,
|
||||
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
|
||||
);
|
||||
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(move_to_next);
|
||||
workspace.register_action(move_to_prev);
|
||||
workspace.register_action(search);
|
||||
workspace.register_action(search_submit);
|
||||
workspace.register_action(search_deploy);
|
||||
|
||||
workspace.register_action(find_command);
|
||||
workspace.register_action(replace_command);
|
||||
}
|
||||
|
||||
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
|
||||
move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
|
||||
}
|
||||
|
||||
fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
|
||||
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
|
||||
}
|
||||
|
||||
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
|
||||
let pane = workspace.active_pane().clone();
|
||||
let direction = if action.backwards {
|
||||
Direction::Prev
|
||||
} else {
|
||||
Direction::Next
|
||||
};
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if !search_bar.show(cx) {
|
||||
return;
|
||||
}
|
||||
let query = search_bar.query(cx);
|
||||
|
||||
search_bar.select_query(cx);
|
||||
cx.focus_self();
|
||||
|
||||
if query.is_empty() {
|
||||
search_bar.set_replacement(None, cx);
|
||||
search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
||||
}
|
||||
vim.workspace_state.search = SearchState {
|
||||
direction,
|
||||
count,
|
||||
initial_query: query.clone(),
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
||||
fn search_deploy(_: &mut Workspace, _: &buffer_search::Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
let state = &mut vim.workspace_state.search;
|
||||
let mut count = state.count;
|
||||
let direction = state.direction;
|
||||
|
||||
// in the case that the query has changed, the search bar
|
||||
// will have selected the next match already.
|
||||
if (search_bar.query(cx) != state.initial_query)
|
||||
&& state.direction == Direction::Next
|
||||
{
|
||||
count = count.saturating_sub(1)
|
||||
}
|
||||
state.count = 1;
|
||||
search_bar.select_match(direction, count, cx);
|
||||
search_bar.focus_editor(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_internal(
|
||||
workspace: &mut Workspace,
|
||||
direction: Direction,
|
||||
whole_word: bool,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
let mut options = SearchOptions::CASE_SENSITIVE;
|
||||
options.set(SearchOptions::WHOLE_WORD, whole_word);
|
||||
if search_bar.show(cx) {
|
||||
search_bar
|
||||
.query_suggestion(cx)
|
||||
.map(|query| search_bar.search(&query, Some(options), cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(search) = search {
|
||||
let search_bar = search_bar.downgrade();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
search.await?;
|
||||
search_bar.update(&mut cx, |search_bar, cx| {
|
||||
search_bar.select_match(direction, count, cx)
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
vim.clear_operator(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
|
||||
let pane = workspace.active_pane().clone();
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
if !search_bar.show(cx) {
|
||||
return None;
|
||||
}
|
||||
let mut query = action.query.clone();
|
||||
if query == "" {
|
||||
query = search_bar.query(cx);
|
||||
};
|
||||
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
||||
Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
|
||||
});
|
||||
let Some(search) = search else { return };
|
||||
let search_bar = search_bar.downgrade();
|
||||
let direction = if action.backwards {
|
||||
Direction::Prev
|
||||
} else {
|
||||
Direction::Next
|
||||
};
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
search.await?;
|
||||
search_bar.update(&mut cx, |search_bar, cx| {
|
||||
search_bar.select_match(direction, 1, cx)
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn replace_command(
|
||||
workspace: &mut Workspace,
|
||||
action: &ReplaceCommand,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let replacement = parse_replace_all(&action.query);
|
||||
let pane = workspace.active_pane().clone();
|
||||
pane.update(cx, |pane, cx| {
|
||||
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
|
||||
return;
|
||||
};
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
if !search_bar.show(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut options = SearchOptions::default();
|
||||
if replacement.is_case_sensitive {
|
||||
options.set(SearchOptions::CASE_SENSITIVE, true)
|
||||
}
|
||||
let search = if replacement.search == "" {
|
||||
search_bar.query(cx)
|
||||
} else {
|
||||
replacement.search
|
||||
};
|
||||
|
||||
search_bar.set_replacement(Some(&replacement.replacement), cx);
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
||||
Some(search_bar.search(&search, Some(options), cx))
|
||||
});
|
||||
let Some(search) = search else { return };
|
||||
let search_bar = search_bar.downgrade();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
search.await?;
|
||||
search_bar.update(&mut cx, |search_bar, cx| {
|
||||
if replacement.should_replace_all {
|
||||
search_bar.select_last_match(cx);
|
||||
search_bar.replace_all(&Default::default(), cx);
|
||||
Vim::update(cx, |vim, cx| {
|
||||
move_cursor(
|
||||
vim,
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
// convert a vim query into something more usable by zed.
|
||||
// we don't attempt to fully convert between the two regex syntaxes,
|
||||
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
|
||||
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
|
||||
fn parse_replace_all(query: &str) -> Replacement {
|
||||
let mut chars = query.chars();
|
||||
if Some('%') != chars.next() || Some('s') != chars.next() {
|
||||
return Replacement::default();
|
||||
}
|
||||
|
||||
let Some(delimeter) = chars.next() else {
|
||||
return Replacement::default();
|
||||
};
|
||||
|
||||
let mut search = String::new();
|
||||
let mut replacement = String::new();
|
||||
let mut flags = String::new();
|
||||
|
||||
let mut buffer = &mut search;
|
||||
|
||||
let mut escaped = false;
|
||||
// 0 - parsing search
|
||||
// 1 - parsing replacement
|
||||
// 2 - parsing flags
|
||||
let mut phase = 0;
|
||||
|
||||
for c in chars {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
if phase == 1 && c.is_digit(10) {
|
||||
buffer.push('$')
|
||||
// unescape escaped parens
|
||||
} else if phase == 0 && c == '(' || c == ')' {
|
||||
} else if c != delimeter {
|
||||
buffer.push('\\')
|
||||
}
|
||||
buffer.push(c)
|
||||
} else if c == '\\' {
|
||||
escaped = true;
|
||||
} else if c == delimeter {
|
||||
if phase == 0 {
|
||||
buffer = &mut replacement;
|
||||
phase = 1;
|
||||
} else if phase == 1 {
|
||||
buffer = &mut flags;
|
||||
phase = 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// escape unescaped parens
|
||||
if phase == 0 && c == '(' || c == ')' {
|
||||
buffer.push('\\')
|
||||
}
|
||||
buffer.push(c)
|
||||
}
|
||||
}
|
||||
|
||||
let mut replacement = Replacement {
|
||||
search,
|
||||
replacement,
|
||||
should_replace_all: true,
|
||||
is_case_sensitive: true,
|
||||
};
|
||||
|
||||
for c in flags.chars() {
|
||||
match c {
|
||||
'g' | 'I' => {}
|
||||
'c' | 'n' => replacement.should_replace_all = false,
|
||||
'i' => replacement.is_case_sensitive = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
replacement
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use editor::DisplayPoint;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["*"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["*"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["#"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["#"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["2", "*"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["g", "*"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["g", "#"]);
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "c", "c"]);
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
cx.update_view(search_bar, |bar, cx| {
|
||||
assert_eq!(bar.query(cx), "cc");
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_text_background_highlights(cx);
|
||||
assert_eq!(3, highlights.len());
|
||||
assert_eq!(
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
|
||||
highlights[0].0
|
||||
)
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
||||
|
||||
// n to go to next/N to go to previous
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["shift-n"]);
|
||||
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
||||
|
||||
// ?<enter> to go to previous
|
||||
cx.simulate_keystrokes(["?", "enter"]);
|
||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["?", "enter"]);
|
||||
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
||||
|
||||
// /<enter> to go to next
|
||||
cx.simulate_keystrokes(["/", "enter"]);
|
||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||
|
||||
// ?{search}<enter> to search backwards
|
||||
cx.simulate_keystrokes(["?", "b", "enter"]);
|
||||
cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||
|
||||
// works with counts
|
||||
cx.simulate_keystrokes(["4", "/", "c"]);
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
|
||||
|
||||
// check that searching resumes from cursor, not previous match
|
||||
cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "d"]);
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
|
||||
cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
|
||||
cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "b"]);
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.set_state("ˇone one one one", Mode::Normal);
|
||||
cx.simulate_keystrokes(["cmd-f"]);
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.assert_editor_state("«oneˇ» one one one");
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
cx.assert_editor_state("one «oneˇ» one one");
|
||||
cx.simulate_keystrokes(["shift-enter"]);
|
||||
cx.assert_editor_state("«oneˇ» one one one");
|
||||
}
|
||||
}
|
@ -1,276 +0,0 @@
|
||||
use editor::movement;
|
||||
use gpui::{actions, ViewContext, WindowContext};
|
||||
use language::Point;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
|
||||
|
||||
actions!(vim, [Substitute, SubstituteLine]);
|
||||
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let count = vim.take_count(cx);
|
||||
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
|
||||
})
|
||||
});
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
let count = vim.take_count(cx);
|
||||
substitute(vim, count, true, cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.start == selection.end {
|
||||
Motion::Right.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
count,
|
||||
true,
|
||||
&text_layout_details,
|
||||
);
|
||||
}
|
||||
if line_mode {
|
||||
// in Visual mode when the selection contains the newline at the end
|
||||
// of the line, we should exclude it.
|
||||
if !selection.is_empty() && selection.end.column() == 0 {
|
||||
selection.end = movement::left(map, selection.end);
|
||||
}
|
||||
Motion::CurrentLine.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
||||
display_lines: false,
|
||||
})
|
||||
.move_point(
|
||||
map,
|
||||
selection.start,
|
||||
selection.goal,
|
||||
None,
|
||||
&text_layout_details,
|
||||
) {
|
||||
selection.start = point;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
copy_selections_content(editor, line_mode, cx);
|
||||
let selections = editor.selections.all::<Point>(cx).into_iter();
|
||||
let edits = selections.map(|selection| (selection.start..selection.end, ""));
|
||||
editor.edit(edits, cx);
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, true, cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_substitute(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// supports a single cursor
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["s", "x"]);
|
||||
cx.assert_editor_state("xˇbc\n");
|
||||
|
||||
// supports a selection
|
||||
cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
|
||||
cx.assert_editor_state("a«bcˇ»\n");
|
||||
cx.simulate_keystrokes(["s", "x"]);
|
||||
cx.assert_editor_state("axˇ\n");
|
||||
|
||||
// supports counts
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["2", "s", "x"]);
|
||||
cx.assert_editor_state("xˇc\n");
|
||||
|
||||
// supports multiple cursors
|
||||
cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["2", "s", "x"]);
|
||||
cx.assert_editor_state("axˇdexˇg\n");
|
||||
|
||||
// does not read beyond end of line
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["5", "s", "x"]);
|
||||
cx.assert_editor_state("xˇ\n");
|
||||
|
||||
// it handles multibyte characters
|
||||
cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["4", "s"]);
|
||||
cx.assert_editor_state("ˇ\n");
|
||||
|
||||
// should transactionally undo selection changes
|
||||
cx.simulate_keystrokes(["escape", "u"]);
|
||||
cx.assert_editor_state("ˇcàfé\n");
|
||||
|
||||
// it handles visual line mode
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
alpha
|
||||
beˇta
|
||||
gamma"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["shift-v", "s"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
alpha
|
||||
ˇ
|
||||
gamma"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("The quick ˇbrown").await;
|
||||
cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
|
||||
cx.assert_shared_state("The quick ˇ").await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
let cases = cx.each_marked_position(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps ˇover
|
||||
the ˇlazy dog"});
|
||||
for initial_state in cases {
|
||||
cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
|
||||
.await;
|
||||
cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["shift-v", "c"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on change
|
||||
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
let mut cx = cx.binding(["shift-v", "j", "c"]);
|
||||
cx.assert(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||
cx.assert_state_matches().await;
|
||||
|
||||
cx.assert_all(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the laˇzy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
let initial_state = indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog
|
||||
"};
|
||||
|
||||
// normal mode
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
the lazy dog
|
||||
"})
|
||||
.await;
|
||||
|
||||
// visual mode
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
oˇ
|
||||
the lazy dog
|
||||
"})
|
||||
.await;
|
||||
|
||||
// visual block mode
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
"})
|
||||
.await;
|
||||
|
||||
// visual mode including newline
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
the lazy dog
|
||||
"})
|
||||
.await;
|
||||
|
||||
// indentation
|
||||
cx.set_neovim_option("shiftwidth=4").await;
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
the lazy dog
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
|
||||
use collections::HashMap;
|
||||
use gpui::WindowContext;
|
||||
|
||||
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_position = (selection.head(), selection.goal);
|
||||
original_positions.insert(selection.id, original_position);
|
||||
motion.expand_selection(map, selection, times, true, &text_layout_details);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
let (head, goal) = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(head, goal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_position = (selection.head(), selection.goal);
|
||||
object.expand_selection(map, selection, around);
|
||||
original_positions.insert(selection.id, original_position);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, false, cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
let (head, goal) = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(head, goal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,234 +0,0 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{Action, KeyContext};
|
||||
use language::CursorShape;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::searchable::Direction;
|
||||
|
||||
use crate::motion::Motion;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
Visual,
|
||||
VisualLine,
|
||||
VisualBlock,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn is_visual(&self) -> bool {
|
||||
match self {
|
||||
Mode::Normal | Mode::Insert => false,
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
pub enum Operator {
|
||||
Change,
|
||||
Delete,
|
||||
Yank,
|
||||
Replace,
|
||||
Object { around: bool },
|
||||
FindForward { before: bool },
|
||||
FindBackward { after: bool },
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct EditorState {
|
||||
pub mode: Mode,
|
||||
pub last_mode: Mode,
|
||||
|
||||
/// pre_count is the number before an operator is specified (3 in 3d2d)
|
||||
pub pre_count: Option<usize>,
|
||||
/// post_count is the number after an operator is specified (2 in 3d2d)
|
||||
pub post_count: Option<usize>,
|
||||
|
||||
pub operator_stack: Vec<Operator>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub enum RecordedSelection {
|
||||
#[default]
|
||||
None,
|
||||
Visual {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
},
|
||||
SingleLine {
|
||||
cols: u32,
|
||||
},
|
||||
VisualBlock {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
},
|
||||
VisualLine {
|
||||
rows: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
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 recorded_actions: Vec<ReplayableAction>,
|
||||
pub recorded_selection: RecordedSelection,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub struct SearchState {
|
||||
pub direction: Direction,
|
||||
pub count: usize,
|
||||
pub initial_query: String,
|
||||
}
|
||||
|
||||
impl Default for SearchState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
direction: Direction::Next,
|
||||
count: 1,
|
||||
initial_query: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorState {
|
||||
pub fn cursor_shape(&self) -> CursorShape {
|
||||
match self.mode {
|
||||
Mode::Normal => {
|
||||
if self.operator_stack.is_empty() {
|
||||
CursorShape::Block
|
||||
} else {
|
||||
CursorShape::Underscore
|
||||
}
|
||||
}
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block,
|
||||
Mode::Insert => CursorShape::Bar,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vim_controlled(&self) -> bool {
|
||||
!matches!(self.mode, Mode::Insert)
|
||||
|| matches!(
|
||||
self.operator_stack.last(),
|
||||
Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
|
||||
)
|
||||
}
|
||||
|
||||
pub fn should_autoindent(&self) -> bool {
|
||||
!(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock)
|
||||
}
|
||||
|
||||
pub fn clip_at_line_ends(&self) -> bool {
|
||||
match self.mode {
|
||||
Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => false,
|
||||
Mode::Normal => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_operator(&self) -> Option<Operator> {
|
||||
self.operator_stack.last().copied()
|
||||
}
|
||||
|
||||
pub fn keymap_context_layer(&self) -> KeyContext {
|
||||
let mut context = KeyContext::default();
|
||||
context.add("VimEnabled");
|
||||
context.set(
|
||||
"vim_mode",
|
||||
match self.mode {
|
||||
Mode::Normal => "normal",
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual",
|
||||
Mode::Insert => "insert",
|
||||
},
|
||||
);
|
||||
|
||||
if self.vim_controlled() {
|
||||
context.add("VimControl");
|
||||
}
|
||||
|
||||
if self.active_operator().is_none() && self.pre_count.is_some()
|
||||
|| self.active_operator().is_some() && self.post_count.is_some()
|
||||
{
|
||||
context.add("VimCount");
|
||||
}
|
||||
|
||||
let active_operator = self.active_operator();
|
||||
|
||||
if let Some(active_operator) = active_operator {
|
||||
for context_flag in active_operator.context_flags().into_iter() {
|
||||
context.add(*context_flag);
|
||||
}
|
||||
}
|
||||
|
||||
context.set(
|
||||
"vim_operator",
|
||||
active_operator.map(|op| op.id()).unwrap_or_else(|| "none"),
|
||||
);
|
||||
|
||||
context
|
||||
}
|
||||
}
|
||||
|
||||
impl Operator {
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Operator::Object { around: false } => "i",
|
||||
Operator::Object { around: true } => "a",
|
||||
Operator::Change => "c",
|
||||
Operator::Delete => "d",
|
||||
Operator::Yank => "y",
|
||||
Operator::Replace => "r",
|
||||
Operator::FindForward { before: false } => "f",
|
||||
Operator::FindForward { before: true } => "t",
|
||||
Operator::FindBackward { after: false } => "F",
|
||||
Operator::FindBackward { after: true } => "T",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context_flags(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Operator::Object { .. } => &["VimObject"],
|
||||
Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace => {
|
||||
&["VimWaiting"]
|
||||
}
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
}
|
@ -1,752 +0,0 @@
|
||||
mod neovim_backed_binding_test_context;
|
||||
mod neovim_backed_test_context;
|
||||
mod neovim_connection;
|
||||
mod vim_test_context;
|
||||
|
||||
use command_palette::CommandPalette;
|
||||
use editor::DisplayPoint;
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
pub use neovim_backed_test_context::*;
|
||||
pub use vim_test_context::*;
|
||||
|
||||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, ModeIndicator};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate_shared_keystroke("i").await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.simulate_shared_keystrokes([
|
||||
"shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
|
||||
])
|
||||
.await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.assert_editor_state("ˇtest");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
|
||||
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
||||
cx.set_state("«hjklˇ»", Mode::Normal);
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.blur());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
cx.update_editor(|_, cx| cx.focus_self());
|
||||
cx.assert_editor_state("«hjklˇ»");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
||||
cx.assert_editor_state("hˇjkl");
|
||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("hTestˇjkl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("/");
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
cx.update_view(search_bar, |bar, cx| {
|
||||
assert_eq!(bar.query(cx), "");
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_count_down(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["2", "down"]);
|
||||
cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
|
||||
cx.simulate_keystrokes(["9", "down"]);
|
||||
cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// goes to end by default
|
||||
cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["shift-g"]);
|
||||
cx.assert_editor_state("aa\nbb\ncˇc");
|
||||
|
||||
// can go to line 1 (https://github.com/zed-industries/community/issues/710)
|
||||
cx.simulate_keystrokes(["1", "shift-g"]);
|
||||
cx.assert_editor_state("aˇa\nbb\ncc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// works in normal mode
|
||||
cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal);
|
||||
cx.simulate_keystrokes([">", ">"]);
|
||||
cx.assert_editor_state("aa\n bˇb\ncc");
|
||||
cx.simulate_keystrokes(["<", "<"]);
|
||||
cx.assert_editor_state("aa\nbˇb\ncc");
|
||||
|
||||
// works in visuial mode
|
||||
cx.simulate_keystrokes(["shift-v", "down", ">"]);
|
||||
cx.assert_editor_state("aa\n b«b\n ccˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aˇbc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["i", "cmd-shift-p"]);
|
||||
|
||||
assert!(cx.workspace(|workspace, cx| workspace.active_modal::<CommandPalette>(cx).is_some()));
|
||||
cx.simulate_keystroke("escape");
|
||||
cx.run_until_parked();
|
||||
assert!(!cx.workspace(|workspace, cx| workspace.active_modal::<CommandPalette>(cx).is_some()));
|
||||
cx.assert_state("aˇbc\n", Mode::Insert);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_escape_cancels(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aˇbˇc", Mode::Normal);
|
||||
cx.simulate_keystrokes(["escape"]);
|
||||
|
||||
cx.assert_state("aˇbc", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "c", "c"]);
|
||||
|
||||
let search_bar = cx.workspace(|workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.expect("Buffer search bar should be deployed")
|
||||
});
|
||||
|
||||
cx.update_view(search_bar, |bar, cx| {
|
||||
assert_eq!(bar.query(cx), "cc");
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_text_background_highlights(cx);
|
||||
assert_eq!(3, highlights.len());
|
||||
assert_eq!(
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
|
||||
highlights[0].0
|
||||
)
|
||||
});
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
|
||||
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["shift-n"]);
|
||||
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_status_indicator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let mode_indicator = cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
|
||||
assert!(mode_indicator.is_some());
|
||||
mode_indicator.unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Some(Mode::Normal)
|
||||
);
|
||||
|
||||
// shows the correct mode
|
||||
cx.simulate_keystrokes(["i"]);
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Some(Mode::Insert)
|
||||
);
|
||||
|
||||
// shows even in search
|
||||
cx.simulate_keystrokes(["escape", "v", "/"]);
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Some(Mode::Visual)
|
||||
);
|
||||
|
||||
// hides if vim mode is disabled
|
||||
cx.disable_vim();
|
||||
cx.run_until_parked();
|
||||
cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
|
||||
assert!(mode_indicator.read(cx).mode.is_none());
|
||||
});
|
||||
|
||||
cx.enable_vim();
|
||||
cx.run_until_parked();
|
||||
cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>().unwrap();
|
||||
assert!(mode_indicator.read(cx).mode.is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_word_characters(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new_typescript(cx).await;
|
||||
cx.set_state(
|
||||
indoc! { "
|
||||
class A {
|
||||
#ˇgoop = 99;
|
||||
$ˇgoop () { return this.#gˇoop };
|
||||
};
|
||||
console.log(new A().$gooˇp())
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["v", "i", "w"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
class A {
|
||||
«#goopˇ» = 99;
|
||||
«$goopˇ» () { return this.«#goopˇ» };
|
||||
};
|
||||
console.log(new A().«$goopˇ»())
|
||||
"},
|
||||
Mode::Visual,
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_join_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
oneˇ two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one two threeˇ four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "v", "3", "j", "shift-j"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one
|
||||
two three fourˇ five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_wrap(12).await;
|
||||
// tests line wrap as follows:
|
||||
// 1: twelve char
|
||||
// twelve char
|
||||
// 2: twelve char
|
||||
cx.set_shared_state(indoc! { "
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve char
|
||||
tˇwelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["k"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["g", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char tˇwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["g", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve char
|
||||
tˇwelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "k"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char tˇwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "^"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char ˇtwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["^"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
ˇtwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "$"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve charˇ twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["$"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve chaˇr
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["enter"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve char
|
||||
ˇtwelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
twelve char
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["o", "o", "escape"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
twelve char twelve char
|
||||
ˇo
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
twelve char
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-a", "a", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
twelve char twelve charˇa
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-i", "i", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
ˇitwelve char twelve chara
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-d"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
ˇ
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
twelve char
|
||||
twelve char tˇwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-o", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
ˇo
|
||||
twelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
// line wraps as:
|
||||
// fourteen ch
|
||||
// ar
|
||||
// fourteen ch
|
||||
// ar
|
||||
cx.set_shared_state(indoc! { "
|
||||
fourteen chaˇr
|
||||
fourteen char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["d", "i", "w"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
fourteenˇ•
|
||||
fourteen char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "shift-f", "e", "f", "r"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
fourteen•
|
||||
fourteen chaˇr
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_folds(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_neovim_option("foldmethod=manual").await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
ˇbarp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
|
||||
.await;
|
||||
|
||||
// visual display is now:
|
||||
// fn boop () {
|
||||
// [FOLDED]
|
||||
// }
|
||||
|
||||
// TODO: this should not be needed but currently zf does not
|
||||
// return to normal mode.
|
||||
cx.simulate_shared_keystrokes(["escape"]).await;
|
||||
|
||||
// skip over fold downward
|
||||
cx.simulate_shared_keystrokes(["g", "g"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
ˇfn boop() {
|
||||
barp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["j", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
barp()
|
||||
bazp()
|
||||
ˇ}
|
||||
"})
|
||||
.await;
|
||||
|
||||
// skip over fold upward
|
||||
cx.simulate_shared_keystrokes(["2", "k"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
ˇfn boop() {
|
||||
barp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
|
||||
// yank the fold
|
||||
cx.simulate_shared_keystrokes(["down", "y", "y"]).await;
|
||||
cx.assert_shared_clipboard(" barp()\n bazp()\n").await;
|
||||
|
||||
// re-open
|
||||
cx.simulate_shared_keystrokes(["z", "o"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
ˇ barp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_folds_panic(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_neovim_option("foldmethod=manual").await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
ˇbarp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["escape"]).await;
|
||||
cx.simulate_shared_keystrokes(["g", "g"]).await;
|
||||
cx.simulate_shared_keystrokes(["5", "d", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "ˇ"}).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇ over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_zero(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["0"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇThe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["1", "0", "l"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick ˇbrown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
;;ˇ;
|
||||
Lorem Ipsum"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["a", "down", "up", ";", "down", "up"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
;;;;ˇ
|
||||
Lorem Ipsum"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_wrap(12).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
aaˇaa
|
||||
😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
aaaa
|
||||
😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
123456789012aaˇaa
|
||||
123456789012😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
123456789012aaaa
|
||||
123456789012😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
123456789012aaˇaa
|
||||
123456789012😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
123456789012aaaa
|
||||
123456789012😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
123456789012aaaaˇaaaaaaaa123456789012
|
||||
wow
|
||||
123456789012😃😃😃😃😃😃123456789012"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
123456789012aaaaaaaaaaaa123456789012
|
||||
wow
|
||||
123456789012😃😃ˇ😃😃😃😃123456789012"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
one
|
||||
ˇ
|
||||
two"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["}", "}"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one
|
||||
|
||||
twˇo"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["{", "{", "{"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇone
|
||||
|
||||
two"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
defmodule Test do
|
||||
def test(a, ˇ[_, _] = b), do: IO.puts('hi')
|
||||
end
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["g", "a"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
defmodule Test do
|
||||
def test(a, «[ˇ»_, _] = b), do: IO.puts('hi')
|
||||
end
|
||||
"},
|
||||
Mode::Visual,
|
||||
);
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES};
|
||||
|
||||
pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
|
||||
cx: NeovimBackedTestContext<'a>,
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
pub fn new(
|
||||
keystrokes_under_test: [&'static str; COUNT],
|
||||
cx: NeovimBackedTestContext<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
keystrokes_under_test,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(self) -> NeovimBackedTestContext<'a> {
|
||||
self.cx
|
||||
}
|
||||
|
||||
pub fn binding<const NEW_COUNT: usize>(
|
||||
self,
|
||||
keystrokes: [&'static str; NEW_COUNT],
|
||||
) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
|
||||
self.consume().binding(keystrokes)
|
||||
}
|
||||
|
||||
pub async fn assert(&mut self, marked_positions: &str) {
|
||||
self.cx
|
||||
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn assert_exempted(&mut self, marked_positions: &str, feature: ExemptionFeatures) {
|
||||
if SUPPORTED_FEATURES.contains(&feature) {
|
||||
self.cx
|
||||
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_manual(
|
||||
&mut self,
|
||||
initial_state: &str,
|
||||
mode_before: Mode,
|
||||
state_after: &str,
|
||||
mode_after: Mode,
|
||||
) {
|
||||
self.cx.assert_binding(
|
||||
self.keystrokes_under_test,
|
||||
initial_state,
|
||||
mode_before,
|
||||
state_after,
|
||||
mode_after,
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn assert_all(&mut self, marked_positions: &str) {
|
||||
self.cx
|
||||
.assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn assert_all_exempted(
|
||||
&mut self,
|
||||
marked_positions: &str,
|
||||
feature: ExemptionFeatures,
|
||||
) {
|
||||
if SUPPORTED_FEATURES.contains(&feature) {
|
||||
self.cx
|
||||
.assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
type Target = NeovimBackedTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
@ -1,439 +0,0 @@
|
||||
use editor::{scroll::VERTICAL_SCROLL_MARGIN, test::editor_test_context::ContextHandle};
|
||||
use gpui::{px, size, Context};
|
||||
use indoc::indoc;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
panic, thread,
|
||||
};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use language::language_settings::{AllLanguageSettings, SoftWrap};
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
use crate::state::Mode;
|
||||
|
||||
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
|
||||
|
||||
/// Enum representing features we have tests for but which don't work, yet. Used
|
||||
/// to add exemptions and automatically
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum ExemptionFeatures {
|
||||
// MOTIONS
|
||||
// When an operator completes at the end of the file, an extra newline is left
|
||||
OperatorLastNewlineRemains,
|
||||
|
||||
// OBJECTS
|
||||
// Resulting position after the operation is slightly incorrect for unintuitive reasons.
|
||||
IncorrectLandingPosition,
|
||||
// Operator around the text object at the end of the line doesn't remove whitespace.
|
||||
AroundObjectLeavesWhitespaceAtEndOfLine,
|
||||
// Sentence object on empty lines
|
||||
SentenceOnEmptyLines,
|
||||
// Whitespace isn't included with text objects at the start of the line
|
||||
SentenceAtStartOfLineWithWhitespace,
|
||||
// Whitespace around sentences is slightly incorrect when starting between sentences
|
||||
AroundSentenceStartingBetweenIncludesWrongWhitespace,
|
||||
// Non empty selection with text objects in visual mode
|
||||
NonEmptyVisualTextObjects,
|
||||
// Sentence Doesn't backtrack when its at the end of the file
|
||||
SentenceAfterPunctuationAtEndOfFile,
|
||||
}
|
||||
|
||||
impl ExemptionFeatures {
|
||||
pub fn supported(&self) -> bool {
|
||||
SUPPORTED_FEATURES.contains(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NeovimBackedTestContext<'a> {
|
||||
cx: VimTestContext<'a>,
|
||||
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
||||
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||
neovim: NeovimConnection,
|
||||
|
||||
last_set_state: Option<String>,
|
||||
recent_keystrokes: Vec<String>,
|
||||
|
||||
is_dirty: bool,
|
||||
}
|
||||
|
||||
impl<'a> NeovimBackedTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
|
||||
// rust stores the name of the test on the current thread.
|
||||
// We use this to automatically name a file that will store
|
||||
// the neovim connection's requests/responses so that we can
|
||||
// run without neovim on CI.
|
||||
let thread = thread::current();
|
||||
let test_name = thread
|
||||
.name()
|
||||
.expect("thread is not named")
|
||||
.split(":")
|
||||
.last()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
cx: VimTestContext::new(cx, true).await,
|
||||
exemptions: Default::default(),
|
||||
neovim: NeovimConnection::new(test_name).await,
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
is_dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_initial_state_exemptions(
|
||||
&mut self,
|
||||
marked_positions: &str,
|
||||
missing_feature: ExemptionFeatures, // Feature required to support this exempted test case
|
||||
) {
|
||||
if !missing_feature.supported() {
|
||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||
|
||||
for cursor_offset in cursor_offsets.iter() {
|
||||
let mut marked_text = unmarked_text.clone();
|
||||
marked_text.insert(*cursor_offset, 'ˇ');
|
||||
|
||||
// None represents all key bindings being exempted for that initial state
|
||||
self.exemptions.insert(marked_text, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
self.simulate_keystroke(keystroke_text)
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystroke_texts: [&str; COUNT],
|
||||
) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.recent_keystrokes.push(keystroke_text.to_string());
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
}
|
||||
self.simulate_keystrokes(keystroke_texts);
|
||||
}
|
||||
|
||||
pub async fn set_shared_state(&mut self, marked_text: &str) {
|
||||
let mode = if marked_text.contains("»") {
|
||||
Mode::Visual
|
||||
} else {
|
||||
Mode::Normal
|
||||
};
|
||||
self.set_state(marked_text, mode);
|
||||
self.last_set_state = Some(marked_text.to_string());
|
||||
self.recent_keystrokes = Vec::new();
|
||||
self.neovim.set_state(marked_text).await;
|
||||
self.is_dirty = true;
|
||||
}
|
||||
|
||||
pub async fn set_shared_wrap(&mut self, columns: u32) {
|
||||
if columns < 12 {
|
||||
panic!("nvim doesn't support columns < 12")
|
||||
}
|
||||
self.neovim.set_option("wrap").await;
|
||||
self.neovim
|
||||
.set_option(&format!("columns={}", columns))
|
||||
.await;
|
||||
|
||||
self.update(|cx| {
|
||||
cx.update_global(|settings: &mut SettingsStore, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength);
|
||||
settings.defaults.preferred_line_length = Some(columns);
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_scroll_height(&mut self, rows: u32) {
|
||||
// match Zed's scrolling behavior
|
||||
self.neovim
|
||||
.set_option(&format!("scrolloff={}", VERTICAL_SCROLL_MARGIN))
|
||||
.await;
|
||||
// +2 to account for the vim command UI at the bottom.
|
||||
self.neovim.set_option(&format!("lines={}", rows + 2)).await;
|
||||
let (line_height, visible_line_count) = self.editor(|editor, cx| {
|
||||
(
|
||||
editor
|
||||
.style()
|
||||
.unwrap()
|
||||
.text
|
||||
.line_height_in_pixels(cx.rem_size()),
|
||||
editor.visible_line_count().unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = self.window;
|
||||
let margin = self
|
||||
.update_window(window, |_, cx| {
|
||||
cx.viewport_size().height - line_height * visible_line_count
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.simulate_window_resize(
|
||||
self.window,
|
||||
size(px(1000.), margin + (rows as f32) * line_height),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn set_neovim_option(&mut self, option: &str) {
|
||||
self.neovim.set_option(option).await;
|
||||
}
|
||||
|
||||
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
||||
self.is_dirty = false;
|
||||
let marked_text = marked_text.replace("•", " ");
|
||||
let neovim = self.neovim_state().await;
|
||||
let editor = self.editor_state();
|
||||
if neovim == marked_text && neovim == editor {
|
||||
return;
|
||||
}
|
||||
let initial_state = self
|
||||
.last_set_state
|
||||
.as_ref()
|
||||
.unwrap_or(&"N/A".to_string())
|
||||
.clone();
|
||||
|
||||
let message = if neovim != marked_text {
|
||||
"Test is incorrect (currently expected != neovim_state)"
|
||||
} else {
|
||||
"Editor does not match nvim behaviour"
|
||||
};
|
||||
panic!(
|
||||
indoc! {"{}
|
||||
# initial state:
|
||||
{}
|
||||
# keystrokes:
|
||||
{}
|
||||
# currently expected:
|
||||
{}
|
||||
# neovim state:
|
||||
{}
|
||||
# zed state:
|
||||
{}"},
|
||||
message,
|
||||
initial_state,
|
||||
self.recent_keystrokes.join(" "),
|
||||
marked_text.replace(" \n", "•\n"),
|
||||
neovim.replace(" \n", "•\n"),
|
||||
editor.replace(" \n", "•\n")
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn assert_shared_clipboard(&mut self, text: &str) {
|
||||
let neovim = self.neovim.read_register('"').await;
|
||||
let editor = self.read_from_clipboard().unwrap().text().clone();
|
||||
|
||||
if text == neovim && text == editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = if neovim != text {
|
||||
"Test is incorrect (currently expected != neovim)"
|
||||
} else {
|
||||
"Editor does not match nvim behaviour"
|
||||
};
|
||||
|
||||
let initial_state = self
|
||||
.last_set_state
|
||||
.as_ref()
|
||||
.unwrap_or(&"N/A".to_string())
|
||||
.clone();
|
||||
|
||||
panic!(
|
||||
indoc! {"{}
|
||||
# initial state:
|
||||
{}
|
||||
# keystrokes:
|
||||
{}
|
||||
# currently expected:
|
||||
{}
|
||||
# neovim clipboard:
|
||||
{}
|
||||
# zed clipboard:
|
||||
{}"},
|
||||
message,
|
||||
initial_state,
|
||||
self.recent_keystrokes.join(" "),
|
||||
text,
|
||||
neovim,
|
||||
editor
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn neovim_state(&mut self) -> String {
|
||||
self.neovim.marked_text().await
|
||||
}
|
||||
|
||||
pub async fn neovim_mode(&mut self) -> Mode {
|
||||
self.neovim.mode().await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
self.is_dirty = false;
|
||||
let neovim = self.neovim_state().await;
|
||||
let editor = self.editor_state();
|
||||
let initial_state = self
|
||||
.last_set_state
|
||||
.as_ref()
|
||||
.unwrap_or(&"N/A".to_string())
|
||||
.clone();
|
||||
|
||||
if neovim != editor {
|
||||
panic!(
|
||||
indoc! {"Test failed (zed does not match nvim behaviour)
|
||||
# initial state:
|
||||
{}
|
||||
# keystrokes:
|
||||
{}
|
||||
# neovim state:
|
||||
{}
|
||||
# zed state:
|
||||
{}"},
|
||||
initial_state,
|
||||
self.recent_keystrokes.join(" "),
|
||||
neovim,
|
||||
editor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
initial_state: &str,
|
||||
) {
|
||||
if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
|
||||
match possible_exempted_keystrokes {
|
||||
Some(exempted_keystrokes) => {
|
||||
if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
|
||||
// This keystroke was exempted for this insertion text
|
||||
return;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// All keystrokes for this insertion text are exempted
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _state_context = self.set_shared_state(initial_state).await;
|
||||
let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.assert_state_matches().await;
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches_all<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
marked_positions: &str,
|
||||
) {
|
||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||
|
||||
for cursor_offset in cursor_offsets.iter() {
|
||||
let mut marked_text = unmarked_text.clone();
|
||||
marked_text.insert(*cursor_offset, 'ˇ');
|
||||
|
||||
self.assert_binding_matches(keystrokes, &marked_text).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn each_marked_position(&self, marked_positions: &str) -> Vec<String> {
|
||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||
let mut ret = Vec::with_capacity(cursor_offsets.len());
|
||||
|
||||
for cursor_offset in cursor_offsets.iter() {
|
||||
let mut marked_text = unmarked_text.clone();
|
||||
marked_text.insert(*cursor_offset, 'ˇ');
|
||||
ret.push(marked_text)
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn assert_neovim_compatible<const COUNT: usize>(
|
||||
&mut self,
|
||||
marked_positions: &str,
|
||||
keystrokes: [&str; COUNT],
|
||||
) {
|
||||
self.set_shared_state(&marked_positions).await;
|
||||
self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.assert_state_matches().await;
|
||||
}
|
||||
|
||||
pub async fn assert_matches_neovim<const COUNT: usize>(
|
||||
&mut self,
|
||||
marked_positions: &str,
|
||||
keystrokes: [&str; COUNT],
|
||||
result: &str,
|
||||
) {
|
||||
self.set_shared_state(marked_positions).await;
|
||||
self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.assert_shared_state(result).await;
|
||||
}
|
||||
|
||||
pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
marked_positions: &str,
|
||||
feature: ExemptionFeatures,
|
||||
) {
|
||||
if SUPPORTED_FEATURES.contains(&feature) {
|
||||
self.assert_binding_matches_all(keystrokes, marked_positions)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binding<const COUNT: usize>(
|
||||
self,
|
||||
keystrokes: [&'static str; COUNT],
|
||||
) -> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||
NeovimBackedBindingTestContext::new(keystrokes, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for NeovimBackedTestContext<'a> {
|
||||
type Target = VimTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for NeovimBackedTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
// a common mistake in tests is to call set_shared_state when
|
||||
// you mean asswert_shared_state. This notices that and lets
|
||||
// you know.
|
||||
impl<'a> Drop for NeovimBackedTestContext<'a> {
|
||||
fn drop(&mut self) {
|
||||
if self.is_dirty {
|
||||
panic!("Test context was dropped after set_shared_state before assert_shared_state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_state_matches().await;
|
||||
cx.set_shared_state("This is a tesˇt").await;
|
||||
cx.assert_state_matches().await;
|
||||
}
|
||||
}
|
@ -1,599 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "neovim")]
|
||||
use std::{
|
||||
cmp,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use gpui::Keystroke;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use language::Point;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||
};
|
||||
#[cfg(feature = "neovim")]
|
||||
use parking_lot::ReentrantMutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "neovim")]
|
||||
use tokio::{
|
||||
process::{Child, ChildStdin, Command},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::state::Mode;
|
||||
use collections::VecDeque;
|
||||
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
|
||||
// to ensure we are only constructing one neovim connection at a time.
|
||||
#[cfg(feature = "neovim")]
|
||||
static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum NeovimData {
|
||||
Put { state: String },
|
||||
Key(String),
|
||||
Get { state: String, mode: Option<Mode> },
|
||||
ReadRegister { name: char, value: String },
|
||||
SetOption { value: String },
|
||||
}
|
||||
|
||||
pub struct NeovimConnection {
|
||||
data: VecDeque<NeovimData>,
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id: String,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: Child,
|
||||
}
|
||||
|
||||
impl NeovimConnection {
|
||||
pub async fn new(test_case_id: String) -> Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
let handler = NvimHandler {};
|
||||
#[cfg(feature = "neovim")]
|
||||
let (nvim, join_handle, child) = Compat::new(async {
|
||||
// Ensure we don't create neovim connections in parallel
|
||||
let _lock = NEOVIM_LOCK.lock();
|
||||
let (nvim, join_handle, child) = new_child_cmd(
|
||||
&mut Command::new("nvim")
|
||||
.arg("--embed")
|
||||
.arg("--clean")
|
||||
// disable swap (otherwise after about 1000 test runs you run out of swap file names)
|
||||
.arg("-n")
|
||||
// disable writing files (just in case)
|
||||
.arg("-m"),
|
||||
handler,
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to neovim process");
|
||||
|
||||
nvim.ui_attach(100, 100, &UiAttachOptions::default())
|
||||
.await
|
||||
.expect("Could not attach to ui");
|
||||
|
||||
// Makes system act a little more like zed in terms of indentation
|
||||
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
|
||||
.await
|
||||
.expect("Could not set smartindent on startup");
|
||||
|
||||
(nvim, join_handle, child)
|
||||
})
|
||||
.await;
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
data: Default::default(),
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
data: Self::read_test_data(&test_case_id),
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: join_handle,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: child,
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a keystroke to the neovim process.
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
|
||||
if keystroke.key == "<" {
|
||||
keystroke.key = "lt".to_string()
|
||||
}
|
||||
|
||||
let special = keystroke.modifiers.shift
|
||||
|| keystroke.modifiers.control
|
||||
|| keystroke.modifiers.alt
|
||||
|| keystroke.modifiers.command
|
||||
|| keystroke.key.len() > 1;
|
||||
let start = if special { "<" } else { "" };
|
||||
let shift = if keystroke.modifiers.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.modifiers.control {
|
||||
"C-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let alt = if keystroke.modifiers.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.modifiers.command {
|
||||
"D-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let end = if special { ">" } else { "" };
|
||||
|
||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||
|
||||
self.data
|
||||
.push_back(NeovimData::Key(keystroke_text.to_string()));
|
||||
self.nvim
|
||||
.input(&key)
|
||||
.await
|
||||
.expect("Could not input keystroke");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
|
||||
self.data.pop_front();
|
||||
}
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::Key(keystroke_text.to_string())),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_state(&mut self, marked_text: &str) {
|
||||
let (text, selections) = parse_state(&marked_text);
|
||||
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let lines = text
|
||||
.split('\n')
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nvim_buffer
|
||||
.set_lines(0, -1, false, lines)
|
||||
.await
|
||||
.expect("Could not set nvim buffer text");
|
||||
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
|
||||
let nvim_window = self
|
||||
.nvim
|
||||
.get_current_win()
|
||||
.await
|
||||
.expect("Could not get neovim window");
|
||||
|
||||
if selections.len() != 1 {
|
||||
panic!("must have one selection");
|
||||
}
|
||||
let selection = &selections[0];
|
||||
|
||||
let cursor = selection.start;
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
|
||||
if !selection.is_empty() {
|
||||
self.nvim
|
||||
.input("v")
|
||||
.await
|
||||
.expect("could not enter visual mode");
|
||||
|
||||
let cursor = selection.end;
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
|
||||
if let Some(NeovimData::Get { mode, state }) = self.data.back() {
|
||||
if *mode == Some(Mode::Normal) && *state == marked_text {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.data.push_back(NeovimData::Put {
|
||||
state: marked_text.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_state(&mut self, marked_text: &str) {
|
||||
if let Some(NeovimData::Get { mode, state: text }) = self.data.front() {
|
||||
if *mode == Some(Mode::Normal) && *text == marked_text {
|
||||
return;
|
||||
}
|
||||
self.data.pop_front();
|
||||
}
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::Put {
|
||||
state: marked_text.to_string()
|
||||
}),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_option(&mut self, value: &str) {
|
||||
self.nvim
|
||||
.command_output(format!("set {}", value).as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.data.push_back(NeovimData::SetOption {
|
||||
value: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_option(&mut self, value: &str) {
|
||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||
self.data.pop_front();
|
||||
};
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::SetOption {
|
||||
value: value.to_string(),
|
||||
}),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn read_register(&mut self, register: char) -> String {
|
||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||
self.data.pop_front();
|
||||
};
|
||||
if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() {
|
||||
if name == register {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("operation does not match recorded script. re-record with --features=neovim")
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn read_register(&mut self, name: char) -> String {
|
||||
let value = self
|
||||
.nvim
|
||||
.command_output(format!("echo getreg('{}')", name).as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.data.push_back(NeovimData::ReadRegister {
|
||||
name,
|
||||
value: value.clone(),
|
||||
});
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
async fn read_position(&mut self, cmd: &str) -> u32 {
|
||||
self.nvim
|
||||
.command_output(cmd)
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn state(&mut self) -> (Option<Mode>, String) {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let text = nvim_buffer
|
||||
.get_lines(0, -1, false)
|
||||
.await
|
||||
.expect("Could not get buffer text")
|
||||
.join("\n");
|
||||
|
||||
// nvim columns are 1-based, so -1.
|
||||
let mut cursor_row = self.read_position("echo line('.')").await - 1;
|
||||
let mut cursor_col = self.read_position("echo col('.')").await - 1;
|
||||
let mut selection_row = self.read_position("echo line('v')").await - 1;
|
||||
let mut selection_col = self.read_position("echo col('v')").await - 1;
|
||||
let total_rows = self.read_position("echo line('$')").await - 1;
|
||||
|
||||
let nvim_mode_text = self
|
||||
.nvim
|
||||
.get_mode()
|
||||
.await
|
||||
.expect("Could not get mode")
|
||||
.into_iter()
|
||||
.find_map(|(key, value)| {
|
||||
if key.as_str() == Some("mode") {
|
||||
Some(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("Could not find mode value");
|
||||
|
||||
let mode = match nvim_mode_text.as_ref() {
|
||||
"i" => Some(Mode::Insert),
|
||||
"n" => Some(Mode::Normal),
|
||||
"v" => Some(Mode::Visual),
|
||||
"V" => Some(Mode::VisualLine),
|
||||
"\x16" => Some(Mode::VisualBlock),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut selections = Vec::new();
|
||||
// Vim uses the index of the first and last character in the selection
|
||||
// Zed uses the index of the positions between the characters, so we need
|
||||
// to add one to the end in visual mode.
|
||||
match mode {
|
||||
Some(Mode::VisualBlock) if selection_row != cursor_row => {
|
||||
// in zed we fake a block selecrtion by using multiple cursors (one per line)
|
||||
// this code emulates that.
|
||||
// to deal with casees where the selection is not perfectly rectangular we extract
|
||||
// the content of the selection via the "a register to get the shape correctly.
|
||||
self.nvim.input("\"aygv").await.unwrap();
|
||||
let content = self.nvim.command_output("echo getreg('a')").await.unwrap();
|
||||
let lines = content.split("\n").collect::<Vec<_>>();
|
||||
let top = cmp::min(selection_row, cursor_row);
|
||||
let left = cmp::min(selection_col, cursor_col);
|
||||
for row in top..=cmp::max(selection_row, cursor_row) {
|
||||
let content = if row - top >= lines.len() as u32 {
|
||||
""
|
||||
} else {
|
||||
lines[(row - top) as usize]
|
||||
};
|
||||
let line_len = self
|
||||
.read_position(format!("echo strlen(getline({}))", row + 1).as_str())
|
||||
.await;
|
||||
|
||||
if left > line_len {
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = Point::new(row, left);
|
||||
let end = Point::new(row, left + content.len() as u32);
|
||||
if cursor_col >= selection_col {
|
||||
selections.push(start..end)
|
||||
} else {
|
||||
selections.push(end..start)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => {
|
||||
if selection_col > cursor_col {
|
||||
let selection_line_length =
|
||||
self.read_position("echo strlen(getline(line('v')))").await;
|
||||
if selection_line_length > selection_col {
|
||||
selection_col += 1;
|
||||
} else if selection_row < total_rows {
|
||||
selection_col = 0;
|
||||
selection_row += 1;
|
||||
}
|
||||
} else {
|
||||
let cursor_line_length =
|
||||
self.read_position("echo strlen(getline(line('.')))").await;
|
||||
if cursor_line_length > cursor_col {
|
||||
cursor_col += 1;
|
||||
} else if cursor_row < total_rows {
|
||||
cursor_col = 0;
|
||||
cursor_row += 1;
|
||||
}
|
||||
}
|
||||
selections.push(
|
||||
Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col),
|
||||
)
|
||||
}
|
||||
Some(Mode::Insert) | Some(Mode::Normal) | None => selections
|
||||
.push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
|
||||
}
|
||||
|
||||
let ranges = encode_ranges(&text, &selections);
|
||||
let state = NeovimData::Get {
|
||||
mode,
|
||||
state: ranges.clone(),
|
||||
};
|
||||
|
||||
if self.data.back() != Some(&state) {
|
||||
self.data.push_back(state.clone());
|
||||
}
|
||||
|
||||
(mode, ranges)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn state(&mut self) -> (Option<Mode>, String) {
|
||||
if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() {
|
||||
(*mode, raw.to_string())
|
||||
} else {
|
||||
panic!("operation does not match recorded script. re-record with --features=neovim");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mode(&mut self) -> Option<Mode> {
|
||||
self.state().await.0
|
||||
}
|
||||
|
||||
pub async fn marked_text(&mut self) -> String {
|
||||
self.state().await.1
|
||||
}
|
||||
|
||||
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
data_path.push("test_data");
|
||||
data_path.push(format!("{}.json", test_case_id));
|
||||
data_path
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let json = std::fs::read_to_string(path).expect(
|
||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||
);
|
||||
|
||||
let mut result = VecDeque::new();
|
||||
for line in json.lines() {
|
||||
result.push_back(
|
||||
serde_json::from_str(line)
|
||||
.expect("invalid test data. regenerate it with '--features neovim'"),
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn write_test_data(test_case_id: &str, data: &VecDeque<NeovimData>) {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let mut json = Vec::new();
|
||||
for entry in data {
|
||||
serde_json::to_writer(&mut json, entry).unwrap();
|
||||
json.push(b'\n');
|
||||
}
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("could not create test data directory");
|
||||
std::fs::write(path, json).expect("could not write out test data");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Deref for NeovimConnection {
|
||||
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl DerefMut for NeovimConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Drop for NeovimConnection {
|
||||
fn drop(&mut self) {
|
||||
Self::write_test_data(&self.test_case_id, &self.data);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[derive(Clone)]
|
||||
struct NvimHandler {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[async_trait]
|
||||
impl Handler for NvimHandler {
|
||||
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) -> Result<Value, Value> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
async fn handle_notify(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
|
||||
let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
|
||||
let point_ranges = ranges
|
||||
.into_iter()
|
||||
.map(|byte_range| {
|
||||
let mut point_range = Point::zero()..Point::zero();
|
||||
let mut ix = 0;
|
||||
let mut position = Point::zero();
|
||||
for c in text.chars().chain(['\0']) {
|
||||
if ix == byte_range.start {
|
||||
point_range.start = position;
|
||||
}
|
||||
if ix == byte_range.end {
|
||||
point_range.end = position;
|
||||
}
|
||||
let len_utf8 = c.len_utf8();
|
||||
ix += len_utf8;
|
||||
if c == '\n' {
|
||||
position.row += 1;
|
||||
position.column = 0;
|
||||
} else {
|
||||
position.column += len_utf8 as u32;
|
||||
}
|
||||
}
|
||||
point_range
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(text, point_ranges)
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn encode_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String {
|
||||
let byte_ranges = point_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut byte_range = 0..0;
|
||||
let mut ix = 0;
|
||||
let mut position = Point::zero();
|
||||
for c in text.chars().chain(['\0']) {
|
||||
if position == range.start {
|
||||
byte_range.start = ix;
|
||||
}
|
||||
if position == range.end {
|
||||
byte_range.end = ix;
|
||||
}
|
||||
let len_utf8 = c.len_utf8();
|
||||
ix += len_utf8;
|
||||
if c == '\n' {
|
||||
position.row += 1;
|
||||
position.column = 0;
|
||||
} else {
|
||||
position.column += len_utf8 as u32;
|
||||
}
|
||||
}
|
||||
byte_range
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
util::test::generate_marked_text(text, &byte_ranges[..], true)
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use editor::test::{
|
||||
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
|
||||
};
|
||||
use futures::Future;
|
||||
use gpui::{Context, View, VisualContext};
|
||||
use lsp::request;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
||||
pub struct VimTestContext<'a> {
|
||||
cx: EditorLspTestContext<'a>,
|
||||
}
|
||||
|
||||
impl<'a> VimTestContext<'a> {
|
||||
pub fn init(cx: &mut gpui::TestAppContext) {
|
||||
if cx.has_global::<Vim>() {
|
||||
dbg!("OOPS");
|
||||
return;
|
||||
}
|
||||
cx.update(|cx| {
|
||||
search::init(cx);
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
command_palette::init(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
|
||||
Self::init(cx);
|
||||
let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
|
||||
Self::new_with_lsp(lsp, enabled)
|
||||
}
|
||||
|
||||
pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
|
||||
Self::init(cx);
|
||||
Self::new_with_lsp(
|
||||
EditorLspTestContext::new_typescript(Default::default(), cx).await,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
});
|
||||
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
});
|
||||
|
||||
// Setup search toolbars and keypress hook
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
observe_keystrokes(cx);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let buffer_search_bar = cx.new_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
// todo!();
|
||||
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
// toolbar.add_item(project_search_bar, cx);
|
||||
})
|
||||
});
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
let vim_mode_indicator = cx.new_view(ModeIndicator::new);
|
||||
status_bar.add_right_item(vim_mode_indicator, cx);
|
||||
});
|
||||
});
|
||||
|
||||
Self { cx }
|
||||
}
|
||||
|
||||
pub fn update_view<F, T, R>(&mut self, view: View<T>, update: F) -> R
|
||||
where
|
||||
T: 'static,
|
||||
F: FnOnce(&mut T, &mut ViewContext<T>) -> R + 'static,
|
||||
{
|
||||
let window = self.window.clone();
|
||||
self.update_window(window, move |_, cx| view.update(cx, update))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.cx.update_workspace(update)
|
||||
}
|
||||
|
||||
pub fn enable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(true));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(false));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mode(&mut self) -> Mode {
|
||||
self.cx.read(|cx| cx.global::<Vim>().state().mode)
|
||||
}
|
||||
|
||||
pub fn active_operator(&mut self) -> Option<Operator> {
|
||||
self.cx
|
||||
.read(|cx| cx.global::<Vim>().state().operator_stack.last().copied())
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||
let window = self.window;
|
||||
self.cx.set_state(text);
|
||||
self.update_window(window, |_, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(mode, true, cx);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
self.cx.cx.cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
||||
self.assert_editor_state(text);
|
||||
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn assert_binding<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
initial_state: &str,
|
||||
initial_mode: Mode,
|
||||
state_after: &str,
|
||||
mode_after: Mode,
|
||||
) {
|
||||
self.set_state(initial_state, initial_mode);
|
||||
self.cx.simulate_keystrokes(keystrokes);
|
||||
self.cx.assert_editor_state(state_after);
|
||||
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> {
|
||||
type Target = EditorTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for VimTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
use editor::{ClipboardSelection, Editor};
|
||||
use gpui::{AppContext, ClipboardItem};
|
||||
use language::Point;
|
||||
|
||||
pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) {
|
||||
let selections = editor.selections.all_adjusted(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let mut text = String::new();
|
||||
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||
{
|
||||
let mut is_first = true;
|
||||
for selection in selections.iter() {
|
||||
let mut start = selection.start;
|
||||
let end = selection.end;
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
text.push_str("\n");
|
||||
}
|
||||
let initial_len = text.len();
|
||||
|
||||
// if the file does not end with \n, and our line-mode selection ends on
|
||||
// that line, we will have expanded the start of the selection to ensure it
|
||||
// contains a newline (so that delete works as expected). We undo that change
|
||||
// here.
|
||||
let is_last_line = linewise
|
||||
&& end.row == buffer.max_buffer_row()
|
||||
&& buffer.max_point().column > 0
|
||||
&& start.row < buffer.max_buffer_row()
|
||||
&& start == Point::new(start.row, buffer.line_len(start.row));
|
||||
|
||||
if is_last_line {
|
||||
start = Point::new(start.row + 1, 0);
|
||||
}
|
||||
for chunk in buffer.text_for_range(start..end) {
|
||||
text.push_str(chunk);
|
||||
}
|
||||
if is_last_line {
|
||||
text.push_str("\n");
|
||||
}
|
||||
clipboard_selections.push(ClipboardSelection {
|
||||
len: text.len() - initial_len,
|
||||
is_entire_line: linewise,
|
||||
first_line_indent: buffer.indent_size_for_line(start.row).len,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
|
||||
}
|
@ -1,607 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
mod command;
|
||||
mod editor_events;
|
||||
mod insert;
|
||||
mod mode_indicator;
|
||||
mod motion;
|
||||
mod normal;
|
||||
mod object;
|
||||
mod state;
|
||||
mod utils;
|
||||
mod visual;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use command_palette::CommandPaletteInterceptor;
|
||||
use editor::{movement, Editor, EditorEvent, EditorMode};
|
||||
use gpui::{
|
||||
actions, impl_actions, Action, AppContext, EntityId, KeyContext, Subscription, View,
|
||||
ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::normal_replace;
|
||||
use serde::Deserialize;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use visual::{visual_block_motion, visual_replace};
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
use crate::state::ReplayableAction;
|
||||
|
||||
pub struct VimModeSetting(pub bool);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SwitchMode(pub Mode);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct PushOperator(pub Operator);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
struct Number(usize);
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[Tab, Enter, Object, InnerObject, FindForward, FindBackward]
|
||||
);
|
||||
// in the workspace namespace so it's not filtered out when vim is disabled.
|
||||
actions!(workspace, [ToggleVimMode]);
|
||||
|
||||
impl_actions!(vim, [SwitchMode, PushOperator, Number]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(Vim::default());
|
||||
VimModeSetting::register(cx);
|
||||
|
||||
editor_events::init(cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, cx| register(workspace, cx))
|
||||
.detach();
|
||||
|
||||
// Any time settings change, update vim mode to match. The Vim struct
|
||||
// will be initialized as disabled by default, so we filter its commands
|
||||
// out when starting up.
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.hidden_namespaces.insert("vim");
|
||||
});
|
||||
cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
|
||||
vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
|
||||
});
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
|
||||
vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
|
||||
Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx))
|
||||
});
|
||||
workspace.register_action(
|
||||
|_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
|
||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||
},
|
||||
);
|
||||
workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| {
|
||||
Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
|
||||
});
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &Tab, cx| {
|
||||
Vim::active_editor_input_ignored(" ".into(), cx)
|
||||
});
|
||||
|
||||
workspace.register_action(|_: &mut Workspace, _: &Enter, cx| {
|
||||
Vim::active_editor_input_ignored("\n".into(), cx)
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let currently_enabled = VimModeSetting::get_global(cx).0;
|
||||
update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
|
||||
*setting = Some(!currently_enabled)
|
||||
})
|
||||
});
|
||||
|
||||
normal::register(workspace, cx);
|
||||
insert::register(workspace, cx);
|
||||
motion::register(workspace, cx);
|
||||
command::register(workspace, cx);
|
||||
object::register(workspace, cx);
|
||||
visual::register(workspace, cx);
|
||||
}
|
||||
|
||||
pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
cx.observe_keystrokes(|keystroke_event, cx| {
|
||||
if let Some(action) = keystroke_event
|
||||
.action
|
||||
.as_ref()
|
||||
.map(|action| action.boxed_clone())
|
||||
{
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(action.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 action.name().starts_with("vim::") {
|
||||
return;
|
||||
}
|
||||
} else if cx.has_pending_keystrokes() {
|
||||
return;
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, cx| match vim.active_operator() {
|
||||
Some(
|
||||
Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace,
|
||||
) => {}
|
||||
Some(_) => {
|
||||
vim.clear_operator(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Vim {
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
editor_subscription: Option<Subscription>,
|
||||
enabled: bool,
|
||||
editor_states: HashMap<EntityId, EditorState>,
|
||||
workspace_state: WorkspaceState,
|
||||
default_state: EditorState,
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
fn read(cx: &mut AppContext) -> &Self {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
fn update<F, S>(cx: &mut WindowContext, update: F) -> S
|
||||
where
|
||||
F: FnOnce(&mut Self, &mut WindowContext) -> S,
|
||||
{
|
||||
cx.update_global(update)
|
||||
}
|
||||
|
||||
fn set_active_editor(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
|
||||
self.active_editor = Some(editor.clone().downgrade());
|
||||
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let editor = editor.read(cx);
|
||||
if editor.leader_peer_id().is_none() {
|
||||
let newest = editor.selections.newest::<usize>(cx);
|
||||
local_selections_changed(newest, cx);
|
||||
}
|
||||
}
|
||||
EditorEvent::InputIgnored { text } => {
|
||||
Vim::active_editor_input_ignored(text.clone(), cx);
|
||||
Vim::record_insertion(text, None, cx)
|
||||
}
|
||||
EditorEvent::InputHandled {
|
||||
text,
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
|
||||
_ => {}
|
||||
}));
|
||||
|
||||
if self.enabled {
|
||||
let editor = editor.read(cx);
|
||||
let editor_mode = editor.mode();
|
||||
let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
|
||||
|
||||
if editor_mode == EditorMode::Full
|
||||
&& !newest_selection_empty
|
||||
&& self.state().mode == Mode::Normal
|
||||
// When following someone, don't switch vim mode.
|
||||
&& editor.leader_peer_id().is_none()
|
||||
{
|
||||
self.switch_mode(Mode::Visual, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.recorded_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,
|
||||
update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
|
||||
) -> Option<S> {
|
||||
let editor = self.active_editor.clone()?.upgrade()?;
|
||||
Some(editor.update(cx, update))
|
||||
}
|
||||
|
||||
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
self.workspace_state.recorded_actions = Default::default();
|
||||
self.workspace_state.recorded_count = None;
|
||||
|
||||
let selections = self
|
||||
.active_editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| {
|
||||
let editor = editor.read(cx);
|
||||
(
|
||||
editor.selections.oldest::<Point>(cx),
|
||||
editor.selections.newest::<Point>(cx),
|
||||
)
|
||||
});
|
||||
|
||||
if let Some((oldest, newest)) = selections {
|
||||
self.workspace_state.recorded_selection = match self.state().mode {
|
||||
Mode::Visual if newest.end.row == newest.start.row => {
|
||||
RecordedSelection::SingleLine {
|
||||
cols: newest.end.column - newest.start.column,
|
||||
}
|
||||
}
|
||||
Mode::Visual => RecordedSelection::Visual {
|
||||
rows: newest.end.row - newest.start.row,
|
||||
cols: newest.end.column,
|
||||
},
|
||||
Mode::VisualLine => RecordedSelection::VisualLine {
|
||||
rows: newest.end.row - newest.start.row,
|
||||
},
|
||||
Mode::VisualBlock => RecordedSelection::VisualBlock {
|
||||
rows: newest.end.row.abs_diff(oldest.start.row),
|
||||
cols: newest.end.column.abs_diff(oldest.start.column),
|
||||
},
|
||||
_ => RecordedSelection::None,
|
||||
}
|
||||
} else {
|
||||
self.workspace_state.recorded_selection = RecordedSelection::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.stop_recording_after_next_action = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||
self.workspace_state.recording = false;
|
||||
self.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||
self.start_recording(cx);
|
||||
self.stop_recording();
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
|
||||
let state = self.state();
|
||||
let last_mode = state.mode;
|
||||
let prior_mode = state.last_mode;
|
||||
self.update_state(|state| {
|
||||
state.last_mode = last_mode;
|
||||
state.mode = mode;
|
||||
state.operator_stack.clear();
|
||||
});
|
||||
if mode != Mode::Insert {
|
||||
self.take_count(cx);
|
||||
}
|
||||
|
||||
// Sync editor settings like clip mode
|
||||
self.sync_vim_settings(cx);
|
||||
|
||||
if leave_selections {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust selections
|
||||
self.update_active_editor(cx, |editor, cx| {
|
||||
if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
|
||||
{
|
||||
visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
|
||||
}
|
||||
|
||||
editor.change_selections(None, cx, |s| {
|
||||
// we cheat with visual block mode and use multiple cursors.
|
||||
// the cost of this cheat is we need to convert back to a single
|
||||
// cursor whenever vim would.
|
||||
if last_mode == Mode::VisualBlock
|
||||
&& (mode != Mode::VisualBlock && mode != Mode::Insert)
|
||||
{
|
||||
let tail = s.oldest_anchor().tail();
|
||||
let head = s.newest_anchor().head();
|
||||
s.select_anchor_ranges(vec![tail..head]);
|
||||
} else if last_mode == Mode::Insert
|
||||
&& prior_mode == Mode::VisualBlock
|
||||
&& mode != Mode::VisualBlock
|
||||
{
|
||||
let pos = s.first_anchor().head();
|
||||
s.select_anchor_ranges(vec![pos..pos])
|
||||
}
|
||||
|
||||
s.move_with(|map, selection| {
|
||||
if last_mode.is_visual() && !mode.is_visual() {
|
||||
let mut point = selection.head();
|
||||
if !selection.reversed && !selection.is_empty() {
|
||||
point = movement::left(map, selection.head());
|
||||
}
|
||||
selection.collapse_to(point, selection.goal)
|
||||
} else if !last_mode.is_visual() && mode.is_visual() {
|
||||
if selection.is_empty() {
|
||||
selection.end = movement::right(map, selection.start);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
|
||||
if self.active_operator().is_some() {
|
||||
self.update_state(|state| {
|
||||
state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
|
||||
})
|
||||
} else {
|
||||
self.update_state(|state| {
|
||||
state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
|
||||
})
|
||||
}
|
||||
// update the keymap so that 0 works
|
||||
self.sync_vim_settings(cx)
|
||||
}
|
||||
|
||||
fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
return self.workspace_state.recorded_count;
|
||||
}
|
||||
|
||||
let count = if self.state().post_count == None && self.state().pre_count == None {
|
||||
return None;
|
||||
} else {
|
||||
Some(self.update_state(|state| {
|
||||
state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
|
||||
}))
|
||||
};
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.recorded_count = count;
|
||||
}
|
||||
self.sync_vim_settings(cx);
|
||||
count
|
||||
}
|
||||
|
||||
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||
if matches!(
|
||||
operator,
|
||||
Operator::Change | Operator::Delete | Operator::Replace
|
||||
) {
|
||||
self.start_recording(cx)
|
||||
};
|
||||
self.update_state(|state| state.operator_stack.push(operator));
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn maybe_pop_operator(&mut self) -> Option<Operator> {
|
||||
self.update_state(|state| state.operator_stack.pop())
|
||||
}
|
||||
|
||||
fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
|
||||
let popped_operator = self.update_state( |state| state.operator_stack.pop()
|
||||
) .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||
self.sync_vim_settings(cx);
|
||||
popped_operator
|
||||
}
|
||||
fn clear_operator(&mut self, cx: &mut WindowContext) {
|
||||
self.take_count(cx);
|
||||
self.update_state(|state| state.operator_stack.clear());
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn active_operator(&self) -> Option<Operator> {
|
||||
self.state().operator_stack.last().copied()
|
||||
}
|
||||
|
||||
fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match Vim::read(cx).active_operator() {
|
||||
Some(Operator::FindForward { before }) => {
|
||||
let find = Motion::FindForward {
|
||||
before,
|
||||
char: text.chars().next().unwrap(),
|
||||
};
|
||||
Vim::update(cx, |vim, _| {
|
||||
vim.workspace_state.last_find = Some(find.clone())
|
||||
});
|
||||
motion::motion(find, cx)
|
||||
}
|
||||
Some(Operator::FindBackward { after }) => {
|
||||
let find = Motion::FindBackward {
|
||||
after,
|
||||
char: text.chars().next().unwrap(),
|
||||
};
|
||||
Vim::update(cx, |vim, _| {
|
||||
vim.workspace_state.last_find = Some(find.clone())
|
||||
});
|
||||
motion::motion(find, cx)
|
||||
}
|
||||
Some(Operator::Replace) => match Vim::read(cx).state().mode {
|
||||
Mode::Normal => normal_replace(text, cx),
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
|
||||
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
|
||||
if self.enabled != enabled {
|
||||
self.enabled = enabled;
|
||||
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
if self.enabled {
|
||||
filter.hidden_namespaces.remove("vim");
|
||||
} else {
|
||||
filter.hidden_namespaces.insert("vim");
|
||||
}
|
||||
});
|
||||
|
||||
if self.enabled {
|
||||
cx.set_global::<CommandPaletteInterceptor>(Box::new(command::command_interceptor));
|
||||
} else if cx.has_global::<CommandPaletteInterceptor>() {
|
||||
let _ = cx.remove_global::<CommandPaletteInterceptor>();
|
||||
}
|
||||
|
||||
if let Some(active_window) = cx.active_window() {
|
||||
active_window
|
||||
.update(cx, |root_view, cx| {
|
||||
if self.enabled {
|
||||
let active_editor = root_view
|
||||
.downcast::<Workspace>()
|
||||
.ok()
|
||||
.and_then(|workspace| workspace.read(cx).active_item(cx))
|
||||
.and_then(|item| item.downcast::<Editor>());
|
||||
if let Some(active_editor) = active_editor {
|
||||
self.set_active_editor(active_editor, cx);
|
||||
}
|
||||
self.switch_mode(Mode::Normal, false, cx);
|
||||
}
|
||||
self.sync_vim_settings(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &EditorState {
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
if let Some(state) = self.editor_states.get(&active_editor.entity_id()) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
&self.default_state
|
||||
}
|
||||
|
||||
pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
|
||||
let mut state = self.state().clone();
|
||||
let ret = func(&mut state);
|
||||
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
self.editor_states.insert(active_editor.entity_id(), state);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn sync_vim_settings(&self, cx: &mut WindowContext) {
|
||||
let state = self.state();
|
||||
let cursor_shape = state.cursor_shape();
|
||||
|
||||
self.update_active_editor(cx, |editor, cx| {
|
||||
if self.enabled && editor.mode() == EditorMode::Full {
|
||||
editor.set_cursor_shape(cursor_shape, cx);
|
||||
editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
|
||||
editor.set_collapse_matches(true);
|
||||
editor.set_input_enabled(!state.vim_controlled());
|
||||
editor.set_autoindent(state.should_autoindent());
|
||||
editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
|
||||
let context_layer = state.keymap_context_layer();
|
||||
editor.set_keymap_context_layer::<Self>(context_layer, cx);
|
||||
} else {
|
||||
// Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
|
||||
// but we need collapse_matches to persist when the search bar is focused.
|
||||
editor.set_collapse_matches(false);
|
||||
self.unhook_vim_settings(editor, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
editor.set_cursor_shape(CursorShape::Bar, cx);
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.set_input_enabled(true);
|
||||
editor.set_autoindent(true);
|
||||
editor.selections.line_mode = false;
|
||||
|
||||
// we set the VimEnabled context on all editors so that we
|
||||
// can distinguish between vim mode and non-vim mode in the BufferSearchBar.
|
||||
// This is a bit of a hack, but currently the search crate does not depend on vim,
|
||||
// and it seems nice to keep it that way.
|
||||
if self.enabled {
|
||||
let mut context = KeyContext::default();
|
||||
context.add("VimEnabled");
|
||||
editor.set_keymap_context_layer::<Self>(context, cx)
|
||||
} else {
|
||||
editor.remove_keymap_context_layer::<Self>(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for VimModeSetting {
|
||||
const KEY: Option<&'static str> = Some("vim_mode");
|
||||
|
||||
type FileContent = Option<bool>;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
|
||||
default_value.ok_or_else(Self::missing_default)?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
|
||||
if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx);
|
||||
} else {
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +0,0 @@
|
||||
{"Get":{"state":"ˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"This is a tesˇt"}}
|
||||
{"Get":{"state":"This is a tesˇt","mode":"Normal"}}
|
@ -1,6 +0,0 @@
|
||||
{"Put":{"state":"The qˇuick"}}
|
||||
{"Key":"a"}
|
||||
{"Get":{"state":"The quˇick","mode":"Insert"}}
|
||||
{"Put":{"state":"The quicˇk"}}
|
||||
{"Key":"a"}
|
||||
{"Get":{"state":"The quickˇ","mode":"Insert"}}
|
@ -1,54 +0,0 @@
|
||||
{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}}
|
@ -1,9 +0,0 @@
|
||||
{"Put":{"state":"ˇThe quick\nbrown"}}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown"}}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick\nˇbrown"}}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}
|
@ -1,570 +0,0 @@
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
@ -1,24 +0,0 @@
|
||||
{"Put":{"state":"ˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The ˇquick"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quˇick\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nˇ\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
|
@ -1,8 +0,0 @@
|
||||
{"Put":{"state":"The qˇuick\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"0"}
|
||||
{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nˇ\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"0"}
|
||||
{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
|
@ -1,24 +0,0 @@
|
||||
{"Put":{"state":"Teˇst Test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇst Test","mode":"Insert"}}
|
||||
{"Put":{"state":"Test ˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇtest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test1 test2 ˇtest3"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇ\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test-test ˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"Test ˇtest","mode":"Insert"}}
|
@ -1,16 +0,0 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"Tˇst","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"ˇest","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇTest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"ˇTest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test\nˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"Testˇtest","mode":"Insert"}}
|
@ -1,23 +0,0 @@
|
||||
{"Put":{"state":"ˇabC\n"}}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"AˇbC\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ABˇc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
|
||||
{"Key":"~"}
|
||||
{"Put":{"state":"aˇC😀é1*F\n"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇC\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}
|
@ -1,24 +0,0 @@
|
||||
{"Put":{"state":"Teˇst Test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Teˇ Test","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Tˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test tesˇt\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Test tesˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇ\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Test test\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst-test test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-e"}
|
||||
{"Get":{"state":"Test teˇ test","mode":"Insert"}}
|
@ -1,16 +0,0 @@
|
||||
{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
|
@ -1,8 +0,0 @@
|
||||
{"Put":{"state":"The qˇuick\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nˇ\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
|
@ -1,20 +0,0 @@
|
||||
{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
|
@ -1,16 +0,0 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"Tˇst","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"ˇest","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇTest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"ˇTest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test\nˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"Test\nˇtest","mode":"Insert"}}
|
@ -1,16 +0,0 @@
|
||||
{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\nˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}
|
@ -1,16 +0,0 @@
|
||||
{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇ\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}}
|
@ -1,8 +0,0 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"Teˇt","mode":"Insert"}}
|
||||
{"Put":{"state":"Tesˇt"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"Tesˇ","mode":"Insert"}}
|
@ -1,270 +0,0 @@
|
||||
{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown?ˇ Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown?ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}}
|
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Teˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Tˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"Testˇ test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Testˇtest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test tesˇt\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇ\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst-test test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"Test teˇ test","mode":"Insert"}}
|
@ -1,460 +0,0 @@
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇover\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇover\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}}
|
@ -1,7 +0,0 @@
|
||||
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"3"}
|
||||
{"Key":"d"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}}
|
@ -1,17 +0,0 @@
|
||||
{"Put":{"state":"ˇone two three four"}}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Get":{"state":"one twˇo three four","mode":"Normal"}}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"ˇone two three four","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"one two three fˇour","mode":"Normal"}}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"one two threeˇ four","mode":"Normal"}}
|
||||
{"Key":"3"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"oneˇ two three four","mode":"Normal"}}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"one two thˇree four","mode":"Normal"}}
|
@ -1,6 +0,0 @@
|
||||
{"Put":{"state":"ˇa\nb\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"j"}
|
||||
{"Key":"enter"}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"ˇa b\nc","mode":"Normal"}}
|
@ -1,5 +0,0 @@
|
||||
{"Put":{"state":"ˇa\nb\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"3"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\nb\nˇc","mode":"Normal"}}
|
@ -1,22 +0,0 @@
|
||||
{"Put":{"state":"ˇa\nb\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"%"}
|
||||
{"Key":"s"}
|
||||
{"Key":"/"}
|
||||
{"Key":"b"}
|
||||
{"Key":"/"}
|
||||
{"Key":"d"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\nˇd\nc","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"%"}
|
||||
{"Key":"s"}
|
||||
{"Key":":"}
|
||||
{"Key":"."}
|
||||
{"Key":":"}
|
||||
{"Key":"\\"}
|
||||
{"Key":"0"}
|
||||
{"Key":"\\"}
|
||||
{"Key":"0"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"aa\ndd\nˇcc","mode":"Normal"}}
|
@ -1,11 +0,0 @@
|
||||
{"Put":{"state":"ˇa\nb\na\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"/"}
|
||||
{"Key":"b"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"?"}
|
||||
{"Key":"a"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"ˇa\nb\na\nc","mode":"Normal"}}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user