- First round of vim tests

Add `observe_keystrokes` back to gpui2
Allow multiple actions to match a given key event

[[PR Description]]

Release Notes:

- (Added|Fixed|Improved) ...
([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
This commit is contained in:
Conrad Irwin 2023-12-11 15:06:51 -07:00 committed by GitHub
commit d12eb0581a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1430 additions and 1365 deletions

View file

@ -170,6 +170,10 @@ impl<'a> EditorTestContext<'a> {
keystrokes_under_test_handle
}
pub fn run_until_parked(&mut self) {
self.cx.background_executor.run_until_parked();
}
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);

View file

@ -18,10 +18,10 @@ use crate::{
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
WindowHandle, WindowId,
ForegroundExecutor, KeyBinding, Keymap, Keystroke, LayoutId, Menu, PathPromptOptions, Pixels,
Platform, PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription,
SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window,
WindowContext, WindowHandle, WindowId,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
@ -170,6 +170,7 @@ impl App {
pub(crate) type FrameCallback = Box<dyn FnOnce(&mut AppContext)>;
type Handler = Box<dyn FnMut(&mut AppContext) -> bool + 'static>;
type Listener = Box<dyn FnMut(&dyn Any, &mut AppContext) -> bool + 'static>;
type KeystrokeObserver = Box<dyn FnMut(&KeystrokeEvent, &mut WindowContext) + 'static>;
type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()> + 'static>;
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
@ -211,6 +212,7 @@ pub struct AppContext {
pub(crate) observers: SubscriberSet<EntityId, Handler>,
// TypeId is the type of the event that the listener callback expects
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
@ -271,6 +273,7 @@ impl AppContext {
observers: SubscriberSet::new(),
event_listeners: SubscriberSet::new(),
release_listeners: SubscriberSet::new(),
keystroke_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
layout_id_buffer: Default::default(),
@ -962,6 +965,15 @@ impl AppContext {
subscription
}
pub fn observe_keystrokes(
&mut self,
f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
) -> Subscription {
let (subscription, activate) = self.keystroke_observers.insert((), Box::new(f));
activate();
subscription
}
pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
self.text_style_stack.push(text_style);
}
@ -1288,3 +1300,9 @@ pub(crate) struct AnyTooltip {
pub view: AnyView,
pub cursor_offset: Point<Pixels>,
}
#[derive(Debug)]
pub struct KeystrokeEvent {
pub keystroke: Keystroke,
pub action: Option<Box<dyn Action>>,
}

View file

@ -208,7 +208,7 @@ impl DispatchTree {
&mut self,
keystroke: &Keystroke,
context: &[KeyContext],
) -> Option<Box<dyn Action>> {
) -> Vec<Box<dyn Action>> {
if !self.keystroke_matchers.contains_key(context) {
let keystroke_contexts = context.iter().cloned().collect();
self.keystroke_matchers.insert(
@ -218,15 +218,15 @@ impl DispatchTree {
}
let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) {
if let KeyMatch::Some(actions) = keystroke_matcher.match_keystroke(keystroke, context) {
// Clear all pending keystrokes when an action has been found.
for keystroke_matcher in self.keystroke_matchers.values_mut() {
keystroke_matcher.clear_pending();
}
Some(action)
actions
} else {
None
vec![]
}
}

View file

@ -59,7 +59,7 @@ impl KeyBinding {
{
// If the binding is completed, push it onto the matches list
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
KeyMatch::Some(self.action.boxed_clone())
KeyMatch::Some(vec![self.action.boxed_clone()])
} else {
KeyMatch::Pending
}

View file

@ -54,14 +54,14 @@ impl KeystrokeMatcher {
}
let mut pending_key = None;
let mut found_actions = Vec::new();
for binding in keymap.bindings().iter().rev() {
for candidate in keystroke.match_candidates() {
self.pending_keystrokes.push(candidate.clone());
match binding.match_keystrokes(&self.pending_keystrokes, context_stack) {
KeyMatch::Some(action) => {
self.pending_keystrokes.clear();
return KeyMatch::Some(action);
KeyMatch::Some(mut actions) => {
found_actions.append(&mut actions);
}
KeyMatch::Pending => {
pending_key.get_or_insert(candidate);
@ -72,6 +72,11 @@ impl KeystrokeMatcher {
}
}
if !found_actions.is_empty() {
self.pending_keystrokes.clear();
return KeyMatch::Some(found_actions);
}
if let Some(pending_key) = pending_key {
self.pending_keystrokes.push(pending_key);
}
@ -101,7 +106,7 @@ impl KeystrokeMatcher {
pub enum KeyMatch {
None,
Pending,
Some(Box<dyn Action>),
Some(Vec<Box<dyn Action>>),
}
impl KeyMatch {

View file

@ -130,7 +130,7 @@ impl Platform for TestPlatform {
}
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
unimplemented!()
self.active_window.lock().clone()
}
fn open_window(

View file

@ -3,9 +3,9 @@ use crate::{
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style,
SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
@ -495,6 +495,29 @@ impl<'a> WindowContext<'a> {
})
}
pub(crate) fn dispatch_keystroke_observers(
&mut self,
event: &dyn Any,
action: Option<Box<dyn Action>>,
) {
let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() else {
return;
};
self.keystroke_observers
.clone()
.retain(&(), move |callback| {
(callback)(
&KeystrokeEvent {
keystroke: key_down_event.keystroke.clone(),
action: action.as_ref().map(|action| action.boxed_clone()),
},
self,
);
true
});
}
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
/// that are currently on the stack to be returned to the app.
pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
@ -1423,14 +1446,12 @@ impl<'a> WindowContext<'a> {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
if node.context.is_some() {
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
if let Some(found) = self
let mut new_actions = self
.window
.rendered_frame
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &context_stack)
{
actions.push(found.boxed_clone())
}
.dispatch_key(&key_down_event.keystroke, &context_stack);
actions.append(&mut new_actions);
}
context_stack.pop();
@ -1438,11 +1459,13 @@ impl<'a> WindowContext<'a> {
}
for action in actions {
self.dispatch_action_on_node(node_id, action);
self.dispatch_action_on_node(node_id, action.boxed_clone());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(action));
return;
}
}
self.dispatch_keystroke_observers(event, None);
}
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {

View file

@ -17,6 +17,7 @@ pub mod project_search;
pub(crate) mod search_bar;
pub fn init(cx: &mut AppContext) {
menu::init();
buffer_search::init(cx);
project_search::init(cx);
}

View file

@ -1,42 +1,40 @@
use gpui::{div, AnyElement, Element, IntoElement, Render, ViewContext};
use settings::{Settings, SettingsStore};
use gpui::{div, AnyElement, Element, IntoElement, Render, Subscription, ViewContext};
use settings::SettingsStore;
use workspace::{item::ItemHandle, ui::Label, StatusItemView};
use crate::{state::Mode, Vim, 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 {
cx.observe_global::<Vim>(|this, cx| this.set_mode(Vim::read(cx).state().mode, cx))
.detach();
let _subscriptions = vec![
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)),
cx.observe_global::<SettingsStore>(|this, cx| this.update_mode(cx)),
];
cx.observe_global::<SettingsStore>(move |mode_indicator, cx| {
if VimModeSetting::get_global(cx).0 {
mode_indicator.mode = cx
.has_global::<Vim>()
.then(|| cx.global::<Vim>().state().mode);
} else {
mode_indicator.mode.take();
}
})
.detach();
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
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;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,6 @@
#![allow(unused)]
// todo!()
use std::ops::{Deref, DerefMut};
use crate::state::Mode;

View file

@ -1,4 +1,7 @@
use editor::scroll::VERTICAL_SCROLL_MARGIN;
#![allow(unused)]
// todo!()
use editor::{scroll::VERTICAL_SCROLL_MARGIN, test::editor_test_context::ContextHandle};
use indoc::indoc;
use settings::SettingsStore;
use std::{
@ -7,7 +10,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;
@ -151,19 +153,20 @@ impl<'a> NeovimBackedTestContext<'a> {
})
}
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 window = self.window;
let line_height =
self.editor(|editor, cx| editor.style().text.line_height(cx.font_cache()));
// todo!()
// 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 window = self.window;
// let line_height =
// self.editor(|editor, cx| editor.style().text.line_height(cx.font_cache()));
window.simulate_resize(vec2f(1000., (rows as f32) * line_height), &mut self.cx);
}
// window.simulate_resize(vec2f(1000., (rows as f32) * line_height), &mut self.cx);
// }
pub async fn set_neovim_option(&mut self, option: &str) {
self.neovim.set_option(option).await;
@ -211,12 +214,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;

View file

@ -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);

View file

@ -1,3 +1,6 @@
#![allow(unused)]
// todo!()
use std::ops::{Deref, DerefMut};
use editor::test::{
@ -16,11 +19,25 @@ pub struct VimTestContext<'a> {
impl<'a> VimTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
cx.update(|cx| {
search::init(cx);
let settings = SettingsStore::test(cx);
cx.set_global(settings);
command_palette::init(cx);
crate::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> {
cx.update(|cx| {
search::init(cx);
let settings = SettingsStore::test(cx);
cx.set_global(settings);
command_palette::init(cx);
crate::init(cx);
});
Self::new_with_lsp(
EditorLspTestContext::new_typescript(Default::default(), cx).await,
true,
@ -28,12 +45,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));
@ -65,9 +76,11 @@ impl<'a> VimTestContext<'a> {
pub fn update_view<F, T, R>(&mut self, view: View<T>, update: F) -> R
where
F: FnOnce(&mut T, &mut ViewContext<T>) -> R,
T: 'static,
F: FnOnce(&mut T, &mut ViewContext<T>) -> R + 'static,
{
self.update_window(self.window, |_, cx| view.update(cx, update))
let window = self.window.clone();
self.update_window(window, move |_, cx| view.update(cx, update))
.unwrap()
}
@ -75,8 +88,7 @@ impl<'a> VimTestContext<'a> {
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.update_window(self.window, |_, cx| self.cx.workspace.update(cx, update))
.unwrap()
self.cx.update_workspace(update)
}
pub fn enable_vim(&mut self) {
@ -111,7 +123,8 @@ impl<'a> VimTestContext<'a> {
Vim::update(cx, |vim, cx| {
vim.switch_mode(mode, true, cx);
})
});
})
.unwrap();
self.cx.cx.cx.run_until_parked();
}

View file

@ -116,45 +116,43 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
visual::register(workspace, cx);
}
pub fn observe_keystrokes(_: &mut WindowContext) {
// todo!()
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()));
// cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
// if result == &MatchResult::Pending {
// return true;
// }
// if let Some(handled_by) = handled_by {
// Vim::update(cx, |vim, _| {
// if vim.workspace_state.recording {
// vim.workspace_state
// .recorded_actions
// .push(ReplayableAction::Action(handled_by.boxed_clone()));
if vim.workspace_state.stop_recording_after_next_action {
vim.workspace_state.recording = false;
vim.workspace_state.stop_recording_after_next_action = false;
}
}
});
// 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;
}
}
// // Keystroke is handled by the vim system, so continue forward
// if handled_by.namespace() == "vim" {
// return true;
// }
// }
// Vim::update(cx, |vim, cx| match vim.active_operator() {
// Some(
// Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace,
// ) => {}
// Some(_) => {
// vim.clear_operator(cx);
// }
// _ => {}
// });
// true
// })
// .detach()
Vim::update(cx, |vim, cx| match vim.active_operator() {
Some(
Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace,
) => {}
Some(_) => {
vim.clear_operator(cx);
}
_ => {}
});
})
.detach()
}
#[derive(Default)]