mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-08 19:43:11 +00:00
jk (#4189)
Add support for mapping `jk` to escape in vim mode. This changes the behaviour of the keymatches when there are pending matches. Before: Even if there was a pending match, any complete matches would be triggered and the pending state lost. After: If there is a pending match, any complete matches are delayed by 1s, or until more keys are typed. Release Notes: - Added support for mapping `jk` in vim mode ([#2378](https://github.com/zed-industries/community/issues/2378)), ([#176](https://github.com/zed-industries/community/issues/176))
This commit is contained in:
commit
72cb865108
17 changed files with 575 additions and 607 deletions
261
.github/workflows/ci.yml
vendored
261
.github/workflows/ci.yml
vendored
|
@ -1,149 +1,148 @@
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "v[0-9]+.[0-9]+.x"
|
- "v[0-9]+.[0-9]+.x"
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# Allow only one workflow per any non-`main` branch.
|
# Allow only one workflow per any non-`main` branch.
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
style:
|
style:
|
||||||
name: Check formatting, Clippy lints, and spelling
|
name: Check formatting, Clippy lints, and spelling
|
||||||
runs-on:
|
runs-on:
|
||||||
- self-hosted
|
- self-hosted
|
||||||
- test
|
- test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up default .cargo/config.toml
|
- name: Set up default .cargo/config.toml
|
||||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||||
|
|
||||||
- name: Check spelling
|
- name: Check spelling
|
||||||
run: |
|
run: |
|
||||||
if ! which typos > /dev/null; then
|
if ! which typos > /dev/null; then
|
||||||
cargo install typos-cli
|
cargo install typos-cli
|
||||||
fi
|
fi
|
||||||
typos
|
typos
|
||||||
|
|
||||||
- name: Run style checks
|
- name: Run style checks
|
||||||
uses: ./.github/actions/check_style
|
uses: ./.github/actions/check_style
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
runs-on:
|
runs-on:
|
||||||
- self-hosted
|
- self-hosted
|
||||||
- test
|
- test
|
||||||
needs: style
|
steps:
|
||||||
steps:
|
- name: Checkout repo
|
||||||
- name: Checkout repo
|
uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v3
|
with:
|
||||||
with:
|
clean: false
|
||||||
clean: false
|
submodules: "recursive"
|
||||||
submodules: "recursive"
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: ./.github/actions/run_tests
|
uses: ./.github/actions/run_tests
|
||||||
|
|
||||||
- name: Build collab
|
- name: Build collab
|
||||||
run: cargo build -p collab
|
run: cargo build -p collab
|
||||||
|
|
||||||
- name: Build other binaries
|
- name: Build other binaries
|
||||||
run: cargo build --workspace --bins --all-features
|
run: cargo build --workspace --bins --all-features
|
||||||
|
|
||||||
bundle:
|
bundle:
|
||||||
name: Bundle app
|
name: Bundle app
|
||||||
runs-on:
|
runs-on:
|
||||||
- self-hosted
|
- self-hosted
|
||||||
- bundle
|
- bundle
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||||
needs: tests
|
needs: tests
|
||||||
|
env:
|
||||||
|
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||||
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||||
|
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||||
|
steps:
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
clean: false
|
||||||
|
submodules: "recursive"
|
||||||
|
|
||||||
|
- name: Limit target directory size
|
||||||
|
run: script/clear-target-dir-if-larger-than 100
|
||||||
|
|
||||||
|
- name: Determine version and release channel
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
version=$(script/get-crate-version zed)
|
||||||
|
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||||
|
echo "Publishing version: ${version} on release channel ${channel}"
|
||||||
|
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
expected_tag_name=""
|
||||||
|
case ${channel} in
|
||||||
|
stable)
|
||||||
|
expected_tag_name="v${version}";;
|
||||||
|
preview)
|
||||||
|
expected_tag_name="v${version}-pre";;
|
||||||
|
nightly)
|
||||||
|
expected_tag_name="v${version}-nightly";;
|
||||||
|
*)
|
||||||
|
echo "can't publish a release on channel ${channel}"
|
||||||
|
exit 1;;
|
||||||
|
esac
|
||||||
|
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||||
|
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate license file
|
||||||
|
run: script/generate-licenses
|
||||||
|
|
||||||
|
- name: Create app bundle
|
||||||
|
run: script/bundle
|
||||||
|
|
||||||
|
- name: Upload app bundle to workflow run if main branch or specific label
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||||
|
with:
|
||||||
|
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||||
|
path: target/release/Zed.dmg
|
||||||
|
|
||||||
|
- uses: softprops/action-gh-release@v1
|
||||||
|
name: Upload app bundle to release
|
||||||
|
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||||
|
files: target/release/Zed.dmg
|
||||||
|
body: ""
|
||||||
env:
|
env:
|
||||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
|
||||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
|
||||||
steps:
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
clean: false
|
|
||||||
submodules: "recursive"
|
|
||||||
|
|
||||||
- name: Limit target directory size
|
|
||||||
run: script/clear-target-dir-if-larger-than 100
|
|
||||||
|
|
||||||
- name: Determine version and release channel
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
version=$(script/get-crate-version zed)
|
|
||||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
|
||||||
echo "Publishing version: ${version} on release channel ${channel}"
|
|
||||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
expected_tag_name=""
|
|
||||||
case ${channel} in
|
|
||||||
stable)
|
|
||||||
expected_tag_name="v${version}";;
|
|
||||||
preview)
|
|
||||||
expected_tag_name="v${version}-pre";;
|
|
||||||
nightly)
|
|
||||||
expected_tag_name="v${version}-nightly";;
|
|
||||||
*)
|
|
||||||
echo "can't publish a release on channel ${channel}"
|
|
||||||
exit 1;;
|
|
||||||
esac
|
|
||||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
|
||||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate license file
|
|
||||||
run: script/generate-licenses
|
|
||||||
|
|
||||||
- name: Create app bundle
|
|
||||||
run: script/bundle
|
|
||||||
|
|
||||||
- name: Upload app bundle to workflow run if main branch or specific label
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
|
||||||
with:
|
|
||||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
|
||||||
path: target/release/Zed.dmg
|
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@v1
|
|
||||||
name: Upload app bundle to release
|
|
||||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
|
||||||
with:
|
|
||||||
draft: true
|
|
||||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
|
||||||
files: target/release/Zed.dmg
|
|
||||||
body: ""
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ use std::{
|
||||||
atomic::{AtomicBool, Ordering::SeqCst},
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
|
|
||||||
|
@ -5945,3 +5946,26 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
|
assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_cmd_k_left(cx: &mut TestAppContext) {
|
||||||
|
let client = TestServer::start1(cx).await;
|
||||||
|
let (workspace, cx) = client.build_test_workspace(cx).await;
|
||||||
|
|
||||||
|
cx.simulate_keystrokes("cmd-n");
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes("cmd-k left");
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes("cmd-k");
|
||||||
|
// sleep for longer than the timeout in keyboard shortcut handling
|
||||||
|
// to verify that it doesn't fire in this case.
|
||||||
|
cx.executor().advance_clock(Duration::from_secs(2));
|
||||||
|
cx.simulate_keystrokes("left");
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -127,6 +127,11 @@ impl TestServer {
|
||||||
(client_a, client_b, channel_id)
|
(client_a, client_b, channel_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn start1<'a>(cx: &'a mut TestAppContext) -> TestClient {
|
||||||
|
let mut server = Self::start(cx.executor().clone()).await;
|
||||||
|
server.create_client(cx, "user_a").await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn reset(&self) {
|
pub async fn reset(&self) {
|
||||||
self.app_state.db.reset();
|
self.app_state.db.reset();
|
||||||
let epoch = self
|
let epoch = self
|
||||||
|
|
|
@ -1,6 +1,57 @@
|
||||||
|
/// KeyDispatch is where GPUI deals with binding actions to key events.
|
||||||
|
///
|
||||||
|
/// The key pieces to making a key binding work are to define an action,
|
||||||
|
/// implement a method that takes that action as a type parameter,
|
||||||
|
/// and then to register the action during render on a focused node
|
||||||
|
/// with a keymap context:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// actions!(editor,[Undo, Redo]);;
|
||||||
|
///
|
||||||
|
/// impl Editor {
|
||||||
|
/// fn undo(&mut self, _: &Undo, _cx: &mut ViewContext<Self>) { ... }
|
||||||
|
/// fn redo(&mut self, _: &Redo, _cx: &mut ViewContext<Self>) { ... }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Render for Editor {
|
||||||
|
/// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
/// div()
|
||||||
|
/// .track_focus(&self.focus_handle)
|
||||||
|
/// .keymap_context("Editor")
|
||||||
|
/// .on_action(cx.listener(Editor::undo))
|
||||||
|
/// .on_action(cx.listener(Editor::redo))
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///```
|
||||||
|
///
|
||||||
|
/// The keybindings themselves are managed independently by calling cx.bind_keys().
|
||||||
|
/// (Though mostly when developing Zed itself, you just need to add a new line to
|
||||||
|
/// assets/keymaps/default.json).
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// cx.bind_keys([
|
||||||
|
/// KeyBinding::new("cmd-z", Editor::undo, Some("Editor")),
|
||||||
|
/// KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")),
|
||||||
|
/// ])
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// With all of this in place, GPUI will ensure that if you have an Editor that contains
|
||||||
|
/// the focus, hitting cmd-z will Undo.
|
||||||
|
///
|
||||||
|
/// In real apps, it is a little more complicated than this, because typically you have
|
||||||
|
/// several nested views that each register keyboard handlers. In this case action matching
|
||||||
|
/// bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined
|
||||||
|
/// then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace.
|
||||||
|
///
|
||||||
|
/// In GPUI, keybindings are not limited to just single keystrokes, you can define
|
||||||
|
/// sequences by separating the keys with a space:
|
||||||
|
///
|
||||||
|
/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
|
||||||
|
///
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
|
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
|
||||||
KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext,
|
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
|
||||||
};
|
};
|
||||||
use collections::FxHashMap;
|
use collections::FxHashMap;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -272,30 +323,51 @@ impl DispatchTree {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dispatch_key pushses the next keystroke into any key binding matchers.
|
||||||
|
// any matching bindings are returned in the order that they should be dispatched:
|
||||||
|
// * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
|
||||||
|
// * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
|
||||||
|
// binding for "b", the Editor action fires first).
|
||||||
pub fn dispatch_key(
|
pub fn dispatch_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
keystroke: &Keystroke,
|
keystroke: &Keystroke,
|
||||||
context: &[KeyContext],
|
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||||
) -> Vec<Box<dyn Action>> {
|
) -> KeymatchResult {
|
||||||
if !self.keystroke_matchers.contains_key(context) {
|
let mut bindings = SmallVec::<[KeyBinding; 1]>::new();
|
||||||
let keystroke_contexts = context.iter().cloned().collect();
|
let mut pending = false;
|
||||||
self.keystroke_matchers.insert(
|
|
||||||
keystroke_contexts,
|
|
||||||
KeystrokeMatcher::new(self.keymap.clone()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
|
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
|
||||||
if let KeyMatch::Some(actions) = keystroke_matcher.match_keystroke(keystroke, context) {
|
for node_id in dispatch_path {
|
||||||
// Clear all pending keystrokes when an action has been found.
|
let node = self.node(*node_id);
|
||||||
for keystroke_matcher in self.keystroke_matchers.values_mut() {
|
|
||||||
keystroke_matcher.clear_pending();
|
if let Some(context) = node.context.clone() {
|
||||||
|
context_stack.push(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
actions
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while !context_stack.is_empty() {
|
||||||
|
let keystroke_matcher = self
|
||||||
|
.keystroke_matchers
|
||||||
|
.entry(context_stack.clone())
|
||||||
|
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
|
||||||
|
|
||||||
|
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
|
||||||
|
pending = result.pending || pending;
|
||||||
|
for new_binding in result.bindings {
|
||||||
|
match bindings
|
||||||
|
.iter()
|
||||||
|
.position(|el| el.keystrokes.len() < new_binding.keystrokes.len())
|
||||||
|
{
|
||||||
|
Some(idx) => {
|
||||||
|
bindings.insert(idx, new_binding);
|
||||||
|
}
|
||||||
|
None => bindings.push(new_binding),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeymatchResult { bindings, pending }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_pending_keystrokes(&self) -> bool {
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
|
|
|
@ -50,7 +50,7 @@ impl KeyBinding {
|
||||||
if self.keystrokes.as_ref().starts_with(pending_keystrokes) {
|
if self.keystrokes.as_ref().starts_with(pending_keystrokes) {
|
||||||
// If the binding is completed, push it onto the matches list
|
// If the binding is completed, push it onto the matches list
|
||||||
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
||||||
KeyMatch::Some(vec![self.action.boxed_clone()])
|
KeyMatch::Matched
|
||||||
} else {
|
} else {
|
||||||
KeyMatch::Pending
|
KeyMatch::Pending
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
|
use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use smallvec::SmallVec;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub(crate) struct KeystrokeMatcher {
|
pub(crate) struct KeystrokeMatcher {
|
||||||
|
@ -8,6 +9,11 @@ pub(crate) struct KeystrokeMatcher {
|
||||||
keymap_version: KeymapVersion,
|
keymap_version: KeymapVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct KeymatchResult {
|
||||||
|
pub bindings: SmallVec<[KeyBinding; 1]>,
|
||||||
|
pub pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl KeystrokeMatcher {
|
impl KeystrokeMatcher {
|
||||||
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
|
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
|
||||||
let keymap_version = keymap.lock().version();
|
let keymap_version = keymap.lock().version();
|
||||||
|
@ -18,10 +24,6 @@ impl KeystrokeMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_pending(&mut self) {
|
|
||||||
self.pending_keystrokes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_pending_keystrokes(&self) -> bool {
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
!self.pending_keystrokes.is_empty()
|
!self.pending_keystrokes.is_empty()
|
||||||
}
|
}
|
||||||
|
@ -39,7 +41,7 @@ impl KeystrokeMatcher {
|
||||||
&mut self,
|
&mut self,
|
||||||
keystroke: &Keystroke,
|
keystroke: &Keystroke,
|
||||||
context_stack: &[KeyContext],
|
context_stack: &[KeyContext],
|
||||||
) -> KeyMatch {
|
) -> KeymatchResult {
|
||||||
let keymap = self.keymap.lock();
|
let keymap = self.keymap.lock();
|
||||||
// Clear pending keystrokes if the keymap has changed since the last matched keystroke.
|
// Clear pending keystrokes if the keymap has changed since the last matched keystroke.
|
||||||
if keymap.version() != self.keymap_version {
|
if keymap.version() != self.keymap_version {
|
||||||
|
@ -48,7 +50,7 @@ impl KeystrokeMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pending_key = None;
|
let mut pending_key = None;
|
||||||
let mut found_actions = Vec::new();
|
let mut bindings = SmallVec::new();
|
||||||
|
|
||||||
for binding in keymap.bindings().rev() {
|
for binding in keymap.bindings().rev() {
|
||||||
if !keymap.binding_enabled(binding, context_stack) {
|
if !keymap.binding_enabled(binding, context_stack) {
|
||||||
|
@ -58,8 +60,8 @@ impl KeystrokeMatcher {
|
||||||
for candidate in keystroke.match_candidates() {
|
for candidate in keystroke.match_candidates() {
|
||||||
self.pending_keystrokes.push(candidate.clone());
|
self.pending_keystrokes.push(candidate.clone());
|
||||||
match binding.match_keystrokes(&self.pending_keystrokes) {
|
match binding.match_keystrokes(&self.pending_keystrokes) {
|
||||||
KeyMatch::Some(mut actions) => {
|
KeyMatch::Matched => {
|
||||||
found_actions.append(&mut actions);
|
bindings.push(binding.clone());
|
||||||
}
|
}
|
||||||
KeyMatch::Pending => {
|
KeyMatch::Pending => {
|
||||||
pending_key.get_or_insert(candidate);
|
pending_key.get_or_insert(candidate);
|
||||||
|
@ -70,16 +72,21 @@ impl KeystrokeMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_actions.is_empty() {
|
if bindings.len() == 0 && pending_key.is_none() && self.pending_keystrokes.len() > 0 {
|
||||||
self.pending_keystrokes.clear();
|
drop(keymap);
|
||||||
return KeyMatch::Some(found_actions);
|
self.pending_keystrokes.remove(0);
|
||||||
} else if let Some(pending_key) = pending_key {
|
return self.match_keystroke(keystroke, context_stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pending = if let Some(pending_key) = pending_key {
|
||||||
self.pending_keystrokes.push(pending_key);
|
self.pending_keystrokes.push(pending_key);
|
||||||
KeyMatch::Pending
|
true
|
||||||
} else {
|
} else {
|
||||||
self.pending_keystrokes.clear();
|
self.pending_keystrokes.clear();
|
||||||
KeyMatch::None
|
false
|
||||||
}
|
};
|
||||||
|
|
||||||
|
KeymatchResult { bindings, pending }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,386 +94,9 @@ impl KeystrokeMatcher {
|
||||||
/// - KeyMatch::None => No match is valid for this key given any pending keystrokes.
|
/// - KeyMatch::None => No match is valid for this key given any pending keystrokes.
|
||||||
/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys.
|
/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys.
|
||||||
/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses.
|
/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum KeyMatch {
|
pub enum KeyMatch {
|
||||||
None,
|
None,
|
||||||
Pending,
|
Pending,
|
||||||
Some(Vec<Box<dyn Action>>),
|
Matched,
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyMatch {
|
|
||||||
/// Returns true if the match is complete.
|
|
||||||
pub fn is_some(&self) -> bool {
|
|
||||||
matches!(self, KeyMatch::Some(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the matches if the match is complete.
|
|
||||||
pub fn matches(self) -> Option<Vec<Box<dyn Action>>> {
|
|
||||||
match self {
|
|
||||||
KeyMatch::Some(matches) => Some(matches),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for KeyMatch {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(KeyMatch::None, KeyMatch::None) => true,
|
|
||||||
(KeyMatch::Pending, KeyMatch::Pending) => true,
|
|
||||||
(KeyMatch::Some(a), KeyMatch::Some(b)) => {
|
|
||||||
if a.len() != b.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (a, b) in a.iter().zip(b.iter()) {
|
|
||||||
if !a.partial_eq(b.as_ref()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use serde_derive::Deserialize;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::{self as gpui, KeyBindingContextPredicate, Modifiers};
|
|
||||||
use crate::{actions, KeyBinding};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keymap_and_view_ordering() {
|
|
||||||
actions!(test, [EditorAction, ProjectPanelAction]);
|
|
||||||
|
|
||||||
let mut editor = KeyContext::default();
|
|
||||||
editor.add("Editor");
|
|
||||||
|
|
||||||
let mut project_panel = KeyContext::default();
|
|
||||||
project_panel.add("ProjectPanel");
|
|
||||||
|
|
||||||
// Editor 'deeper' in than project panel
|
|
||||||
let dispatch_path = vec![project_panel, editor];
|
|
||||||
|
|
||||||
// But editor actions 'higher' up in keymap
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
KeyBinding::new("left", EditorAction, Some("Editor")),
|
|
||||||
KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
|
|
||||||
|
|
||||||
let matches = matcher
|
|
||||||
.match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path)
|
|
||||||
.matches()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches[0].partial_eq(&EditorAction));
|
|
||||||
assert!(matches.get(1).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multi_keystroke_match() {
|
|
||||||
actions!(test, [B, AB, C, D, DA, E, EF]);
|
|
||||||
|
|
||||||
let mut context1 = KeyContext::default();
|
|
||||||
context1.add("1");
|
|
||||||
|
|
||||||
let mut context2 = KeyContext::default();
|
|
||||||
context2.add("2");
|
|
||||||
|
|
||||||
let dispatch_path = vec![context2, context1];
|
|
||||||
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
KeyBinding::new("a b", AB, Some("1")),
|
|
||||||
KeyBinding::new("b", B, Some("2")),
|
|
||||||
KeyBinding::new("c", C, Some("2")),
|
|
||||||
KeyBinding::new("d", D, Some("1")),
|
|
||||||
KeyBinding::new("d", D, Some("2")),
|
|
||||||
KeyBinding::new("d a", DA, Some("2")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
|
|
||||||
|
|
||||||
// Binding with pending prefix always takes precedence
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
|
|
||||||
KeyMatch::Pending,
|
|
||||||
);
|
|
||||||
// B alone doesn't match because a was pending, so AB is returned instead
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path),
|
|
||||||
KeyMatch::Some(vec![Box::new(AB)]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// Without an a prefix, B is dispatched like expected
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]),
|
|
||||||
KeyMatch::Some(vec![Box::new(B)]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// If a is prefixed, C will not be dispatched because there
|
|
||||||
// was a pending binding for it
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
|
|
||||||
KeyMatch::Pending,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path),
|
|
||||||
KeyMatch::None,
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// If a single keystroke matches multiple bindings in the tree
|
|
||||||
// only one of them is returned.
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path),
|
|
||||||
KeyMatch::Some(vec![Box::new(D)]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keystroke_parsing() {
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("ctrl-p").unwrap(),
|
|
||||||
Keystroke {
|
|
||||||
key: "p".into(),
|
|
||||||
modifiers: Modifiers {
|
|
||||||
control: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
command: false,
|
|
||||||
function: false,
|
|
||||||
},
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("alt-shift-down").unwrap(),
|
|
||||||
Keystroke {
|
|
||||||
key: "down".into(),
|
|
||||||
modifiers: Modifiers {
|
|
||||||
control: false,
|
|
||||||
alt: true,
|
|
||||||
shift: true,
|
|
||||||
command: false,
|
|
||||||
function: false,
|
|
||||||
},
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("shift-cmd--").unwrap(),
|
|
||||||
Keystroke {
|
|
||||||
key: "-".into(),
|
|
||||||
modifiers: Modifiers {
|
|
||||||
control: false,
|
|
||||||
alt: false,
|
|
||||||
shift: true,
|
|
||||||
command: true,
|
|
||||||
function: false,
|
|
||||||
},
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_predicate_parsing() {
|
|
||||||
use KeyBindingContextPredicate::*;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
|
|
||||||
And(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Or(
|
|
||||||
Box::new(Equal("b".into(), "c".into())),
|
|
||||||
Box::new(NotEqual("d".into(), "e".into())),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
KeyBindingContextPredicate::parse("!a").unwrap(),
|
|
||||||
Not(Box::new(Identifier("a".into())),)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_predicate_eval() {
|
|
||||||
let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap();
|
|
||||||
|
|
||||||
let mut context = KeyContext::default();
|
|
||||||
context.add("a");
|
|
||||||
assert!(!predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let mut context = KeyContext::default();
|
|
||||||
context.add("a");
|
|
||||||
context.add("b");
|
|
||||||
assert!(predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let mut context = KeyContext::default();
|
|
||||||
context.add("a");
|
|
||||||
context.set("c", "x");
|
|
||||||
assert!(!predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let mut context = KeyContext::default();
|
|
||||||
context.add("a");
|
|
||||||
context.set("c", "d");
|
|
||||||
assert!(predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let predicate = KeyBindingContextPredicate::parse("!a").unwrap();
|
|
||||||
assert!(predicate.eval(&[KeyContext::default()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_child_predicate_eval() {
|
|
||||||
let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap();
|
|
||||||
let contexts = [
|
|
||||||
context_set(&["a", "b"]),
|
|
||||||
context_set(&["c", "d"]), // match this context
|
|
||||||
context_set(&["e", "f"]),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert!(!predicate.eval(&contexts[..=0]));
|
|
||||||
assert!(predicate.eval(&contexts[..=1]));
|
|
||||||
assert!(!predicate.eval(&contexts[..=2]));
|
|
||||||
|
|
||||||
let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap();
|
|
||||||
let contexts = [
|
|
||||||
context_set(&["a", "b"]),
|
|
||||||
context_set(&["c", "d"]),
|
|
||||||
context_set(&["e"]),
|
|
||||||
context_set(&["a", "b"]),
|
|
||||||
context_set(&["c"]),
|
|
||||||
context_set(&["e"]), // only match this context
|
|
||||||
context_set(&["f"]),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert!(!predicate.eval(&contexts[..=0]));
|
|
||||||
assert!(!predicate.eval(&contexts[..=1]));
|
|
||||||
assert!(!predicate.eval(&contexts[..=2]));
|
|
||||||
assert!(!predicate.eval(&contexts[..=3]));
|
|
||||||
assert!(!predicate.eval(&contexts[..=4]));
|
|
||||||
assert!(predicate.eval(&contexts[..=5]));
|
|
||||||
assert!(!predicate.eval(&contexts[..=6]));
|
|
||||||
|
|
||||||
fn context_set(names: &[&str]) -> KeyContext {
|
|
||||||
let mut keymap = KeyContext::default();
|
|
||||||
names.iter().for_each(|name| keymap.add(name.to_string()));
|
|
||||||
keymap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_matcher() {
|
|
||||||
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
|
|
||||||
pub struct A(pub String);
|
|
||||||
impl_actions!(test, [A]);
|
|
||||||
actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
struct ActionArg {
|
|
||||||
a: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
KeyBinding::new("a", A("x".to_string()), Some("a")),
|
|
||||||
KeyBinding::new("b", B, Some("a")),
|
|
||||||
KeyBinding::new("a b", Ab, Some("a || b")),
|
|
||||||
KeyBinding::new("$", Dollar, Some("a")),
|
|
||||||
KeyBinding::new("\"", Quote, Some("a")),
|
|
||||||
KeyBinding::new("alt-s", Ess, Some("a")),
|
|
||||||
KeyBinding::new("ctrl-`", Backtick, Some("a")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut context_a = KeyContext::default();
|
|
||||||
context_a.add("a");
|
|
||||||
|
|
||||||
let mut context_b = KeyContext::default();
|
|
||||||
context_b.add("b");
|
|
||||||
|
|
||||||
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
|
|
||||||
|
|
||||||
// Basic match
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(A("x".to_string()))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Multi-keystroke match
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]),
|
|
||||||
KeyMatch::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(Ab)])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Failed matches don't interfere with matching subsequent keys
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]),
|
|
||||||
KeyMatch::None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(A("x".to_string()))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
let mut context_c = KeyContext::default();
|
|
||||||
context_c.add("c");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(
|
|
||||||
&Keystroke::parse("a").unwrap(),
|
|
||||||
&[context_c.clone(), context_b.clone()]
|
|
||||||
),
|
|
||||||
KeyMatch::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(Ab)])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle Czech $ (option + 4 key)
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(Dollar)])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle Brazilian quote (quote key then space key)
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(
|
|
||||||
&Keystroke::parse("space->\"").unwrap(),
|
|
||||||
&[context_a.clone()]
|
|
||||||
),
|
|
||||||
KeyMatch::Some(vec![Box::new(Quote)])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle ctrl+` on a brazilian keyboard
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(Backtick)])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle alt-s on a US keyboard
|
|
||||||
assert_eq!(
|
|
||||||
matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]),
|
|
||||||
KeyMatch::Some(vec![Box::new(Ess)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -359,7 +359,7 @@ impl PlatformInputHandler {
|
||||||
self.cx
|
self.cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
self.handler
|
self.handler
|
||||||
.replace_text_in_range(replacement_range, text, cx)
|
.replace_text_in_range(replacement_range, text, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -392,6 +392,13 @@ impl PlatformInputHandler {
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) {
|
||||||
|
let Some(range) = self.handler.selected_text_range(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.handler.replace_text_in_range(Some(range), &input, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zed's interface for handling text input from the platform's IME system
|
/// Zed's interface for handling text input from the platform's IME system
|
||||||
|
|
|
@ -30,24 +30,26 @@ impl Keystroke {
|
||||||
pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
|
pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
|
||||||
let mut possibilities = SmallVec::new();
|
let mut possibilities = SmallVec::new();
|
||||||
match self.ime_key.as_ref() {
|
match self.ime_key.as_ref() {
|
||||||
None => possibilities.push(self.clone()),
|
|
||||||
Some(ime_key) => {
|
Some(ime_key) => {
|
||||||
possibilities.push(Keystroke {
|
if ime_key != &self.key {
|
||||||
modifiers: Modifiers {
|
possibilities.push(Keystroke {
|
||||||
control: self.modifiers.control,
|
modifiers: Modifiers {
|
||||||
alt: false,
|
control: self.modifiers.control,
|
||||||
shift: false,
|
alt: false,
|
||||||
command: false,
|
shift: false,
|
||||||
function: false,
|
command: false,
|
||||||
},
|
function: false,
|
||||||
key: ime_key.to_string(),
|
},
|
||||||
ime_key: None,
|
key: ime_key.to_string(),
|
||||||
});
|
ime_key: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
possibilities.push(Keystroke {
|
possibilities.push(Keystroke {
|
||||||
ime_key: None,
|
ime_key: None,
|
||||||
..self.clone()
|
..self.clone()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
None => possibilities.push(self.clone()),
|
||||||
}
|
}
|
||||||
possibilities
|
possibilities
|
||||||
}
|
}
|
||||||
|
|
|
@ -1542,9 +1542,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
|
||||||
replacement_range,
|
replacement_range,
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
});
|
});
|
||||||
if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key {
|
pending_key_down.0.keystroke.ime_key = Some(text.to_string());
|
||||||
pending_key_down.0.keystroke.ime_key = Some(text.to_string());
|
|
||||||
}
|
|
||||||
window_state.lock().pending_key_down = Some(pending_key_down);
|
window_state.lock().pending_key_down = Some(pending_key_down);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,19 @@ impl TestWindow {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) {
|
pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) {
|
||||||
|
if keystroke.ime_key.is_none()
|
||||||
|
&& !keystroke.modifiers.command
|
||||||
|
&& !keystroke.modifiers.control
|
||||||
|
&& !keystroke.modifiers.function
|
||||||
|
{
|
||||||
|
keystroke.ime_key = Some(if keystroke.modifiers.shift {
|
||||||
|
keystroke.key.to_ascii_uppercase().clone()
|
||||||
|
} else {
|
||||||
|
keystroke.key.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
|
if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
|
||||||
keystroke: keystroke.clone(),
|
keystroke: keystroke.clone(),
|
||||||
is_held,
|
is_held,
|
||||||
|
@ -112,8 +124,9 @@ impl TestWindow {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
drop(lock);
|
drop(lock);
|
||||||
let text = keystroke.ime_key.unwrap_or(keystroke.key);
|
if let Some(text) = keystroke.ime_key.as_ref() {
|
||||||
input_handler.replace_text_in_range(None, &text);
|
input_handler.replace_text_in_range(None, &text);
|
||||||
|
}
|
||||||
|
|
||||||
self.0.lock().input_handler = Some(input_handler);
|
self.0.lock().input_handler = Some(input_handler);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ use crate::{
|
||||||
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
|
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
|
||||||
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
|
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
|
||||||
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
|
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
|
||||||
GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, Model,
|
GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
|
||||||
ModelContext, Modifiers, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas,
|
Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
|
||||||
PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render, ScaledPixels,
|
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
|
||||||
SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task, View, VisualContext,
|
PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
|
||||||
WeakView, WindowBounds, WindowOptions,
|
TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds, WindowOptions,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::FxHashSet;
|
use collections::FxHashSet;
|
||||||
|
@ -33,6 +33,7 @@ use std::{
|
||||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use util::{measure, ResultExt};
|
use util::{measure, ResultExt};
|
||||||
|
|
||||||
|
@ -273,11 +274,47 @@ pub struct Window {
|
||||||
activation_observers: SubscriberSet<(), AnyObserver>,
|
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||||
pub(crate) focus: Option<FocusId>,
|
pub(crate) focus: Option<FocusId>,
|
||||||
focus_enabled: bool,
|
focus_enabled: bool,
|
||||||
|
pending_input: Option<PendingInput>,
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub(crate) focus_invalidated: bool,
|
pub(crate) focus_invalidated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct PendingInput {
|
||||||
|
keystrokes: SmallVec<[Keystroke; 1]>,
|
||||||
|
bindings: SmallVec<[KeyBinding; 1]>,
|
||||||
|
focus: Option<FocusId>,
|
||||||
|
timer: Option<Task<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PendingInput {
|
||||||
|
fn is_noop(&self) -> bool {
|
||||||
|
self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input(&self) -> String {
|
||||||
|
self.keystrokes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|k| k.ime_key.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn used_by_binding(&self, binding: &KeyBinding) -> bool {
|
||||||
|
if self.keystrokes.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let keystroke = &self.keystrokes[0];
|
||||||
|
for candidate in keystroke.match_candidates() {
|
||||||
|
if binding.match_keystrokes(&[candidate]) == KeyMatch::Pending {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct ElementStateBox {
|
pub(crate) struct ElementStateBox {
|
||||||
pub(crate) inner: Box<dyn Any>,
|
pub(crate) inner: Box<dyn Any>,
|
||||||
pub(crate) parent_view_id: EntityId,
|
pub(crate) parent_view_id: EntityId,
|
||||||
|
@ -379,6 +416,7 @@ impl Window {
|
||||||
activation_observers: SubscriberSet::new(),
|
activation_observers: SubscriberSet::new(),
|
||||||
focus: None,
|
focus: None,
|
||||||
focus_enabled: true,
|
focus_enabled: true,
|
||||||
|
pending_input: None,
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
focus_invalidated: false,
|
focus_invalidated: false,
|
||||||
|
@ -1175,44 +1213,67 @@ impl<'a> WindowContext<'a> {
|
||||||
.dispatch_tree
|
.dispatch_tree
|
||||||
.dispatch_path(node_id);
|
.dispatch_path(node_id);
|
||||||
|
|
||||||
let mut actions: Vec<Box<dyn Action>> = Vec::new();
|
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||||
|
let KeymatchResult { bindings, pending } = self
|
||||||
|
.window
|
||||||
|
.rendered_frame
|
||||||
|
.dispatch_tree
|
||||||
|
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
||||||
|
|
||||||
let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
|
if pending {
|
||||||
for node_id in &dispatch_path {
|
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
|
||||||
|
{
|
||||||
if let Some(context) = node.context.clone() {
|
currently_pending = PendingInput::default();
|
||||||
context_stack.push(context);
|
}
|
||||||
}
|
currently_pending.focus = self.window.focus;
|
||||||
}
|
currently_pending
|
||||||
|
.keystrokes
|
||||||
for node_id in dispatch_path.iter().rev() {
|
.push(key_down_event.keystroke.clone());
|
||||||
// Match keystrokes
|
for binding in bindings {
|
||||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
currently_pending.bindings.push(binding);
|
||||||
if node.context.is_some() {
|
|
||||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
|
||||||
let mut new_actions = self
|
|
||||||
.window
|
|
||||||
.rendered_frame
|
|
||||||
.dispatch_tree
|
|
||||||
.dispatch_key(&key_down_event.keystroke, &context_stack);
|
|
||||||
actions.append(&mut new_actions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context_stack.pop();
|
// for vim compatibility, we also should check "is input handler enabled"
|
||||||
}
|
if !currently_pending.is_noop() {
|
||||||
}
|
currently_pending.timer = Some(self.spawn(|mut cx| async move {
|
||||||
|
cx.background_executor.timer(Duration::from_secs(1)).await;
|
||||||
|
cx.update(move |cx| {
|
||||||
|
cx.clear_pending_keystrokes();
|
||||||
|
let Some(currently_pending) = cx.window.pending_input.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
cx.replay_pending_input(currently_pending)
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
currently_pending.timer = None;
|
||||||
|
}
|
||||||
|
self.window.pending_input = Some(currently_pending);
|
||||||
|
|
||||||
if !actions.is_empty() {
|
self.propagate_event = false;
|
||||||
self.clear_pending_keystrokes();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.propagate_event = true;
|
|
||||||
for action in actions {
|
|
||||||
self.dispatch_action_on_node(node_id, action.boxed_clone());
|
|
||||||
if !self.propagate_event {
|
|
||||||
self.dispatch_keystroke_observers(event, Some(action));
|
|
||||||
return;
|
return;
|
||||||
|
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||||
|
if bindings
|
||||||
|
.iter()
|
||||||
|
.all(|binding| !currently_pending.used_by_binding(&binding))
|
||||||
|
{
|
||||||
|
self.replay_pending_input(currently_pending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bindings.is_empty() {
|
||||||
|
self.clear_pending_keystrokes();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.propagate_event = true;
|
||||||
|
for binding in bindings {
|
||||||
|
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
|
||||||
|
if !self.propagate_event {
|
||||||
|
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1255,6 +1316,40 @@ impl<'a> WindowContext<'a> {
|
||||||
.has_pending_keystrokes()
|
.has_pending_keystrokes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replay_pending_input(&mut self, currently_pending: PendingInput) {
|
||||||
|
let node_id = self
|
||||||
|
.window
|
||||||
|
.focus
|
||||||
|
.and_then(|focus_id| {
|
||||||
|
self.window
|
||||||
|
.rendered_frame
|
||||||
|
.dispatch_tree
|
||||||
|
.focusable_node_id(focus_id)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
|
||||||
|
|
||||||
|
if self.window.focus != currently_pending.focus {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = currently_pending.input();
|
||||||
|
|
||||||
|
self.propagate_event = true;
|
||||||
|
for binding in currently_pending.bindings {
|
||||||
|
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
|
||||||
|
if !self.propagate_event {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !input.is_empty() {
|
||||||
|
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
|
||||||
|
input_handler.flush_pending_input(&input, self);
|
||||||
|
self.window.platform_window.set_input_handler(input_handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
|
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
|
||||||
let dispatch_path = self
|
let dispatch_path = self
|
||||||
.window
|
.window
|
||||||
|
|
|
@ -73,9 +73,9 @@ pub(crate) struct Up {
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct Down {
|
pub(crate) struct Down {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
display_lines: bool,
|
pub(crate) display_lines: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
|
|
@ -3,8 +3,11 @@ mod neovim_backed_test_context;
|
||||||
mod neovim_connection;
|
mod neovim_connection;
|
||||||
mod vim_test_context;
|
mod vim_test_context;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use command_palette::CommandPalette;
|
use command_palette::CommandPalette;
|
||||||
use editor::DisplayPoint;
|
use editor::DisplayPoint;
|
||||||
|
use gpui::KeyBinding;
|
||||||
pub use neovim_backed_binding_test_context::*;
|
pub use neovim_backed_binding_test_context::*;
|
||||||
pub use neovim_backed_test_context::*;
|
pub use neovim_backed_test_context::*;
|
||||||
pub use vim_test_context::*;
|
pub use vim_test_context::*;
|
||||||
|
@ -12,7 +15,7 @@ pub use vim_test_context::*;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use search::BufferSearchBar;
|
use search::BufferSearchBar;
|
||||||
|
|
||||||
use crate::{state::Mode, ModeIndicator};
|
use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -774,3 +777,73 @@ async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) {
|
||||||
Mode::Visual,
|
Mode::Visual,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_jk(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.bind_keys([KeyBinding::new(
|
||||||
|
"j k",
|
||||||
|
NormalBefore,
|
||||||
|
Some("vim_mode == insert"),
|
||||||
|
)])
|
||||||
|
});
|
||||||
|
cx.neovim.exec("imap jk <esc>").await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇhello").await;
|
||||||
|
cx.simulate_shared_keystrokes(["i", "j", "o", "j", "k"])
|
||||||
|
.await;
|
||||||
|
cx.assert_shared_state("jˇohello").await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_jk_delay(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.bind_keys([KeyBinding::new(
|
||||||
|
"j k",
|
||||||
|
NormalBefore,
|
||||||
|
Some("vim_mode == insert"),
|
||||||
|
)])
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.set_state("ˇhello", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes(["i", "j"]);
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(500));
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_state("ˇhello", Mode::Insert);
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(500));
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_state("jˇhello", Mode::Insert);
|
||||||
|
cx.simulate_keystrokes(["k", "j", "k"]);
|
||||||
|
cx.assert_state("jˇkhello", Mode::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_comma_w(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.bind_keys([KeyBinding::new(
|
||||||
|
", w",
|
||||||
|
motion::Down {
|
||||||
|
display_lines: false,
|
||||||
|
},
|
||||||
|
Some("vim_mode == normal"),
|
||||||
|
)])
|
||||||
|
});
|
||||||
|
cx.neovim.exec("map ,w j").await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇhello hello\nhello hello").await;
|
||||||
|
cx.simulate_shared_keystrokes(["f", "o", ";", ",", "w"])
|
||||||
|
.await;
|
||||||
|
cx.assert_shared_state("hello hello\nhello hellˇo").await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇhello hello\nhello hello").await;
|
||||||
|
cx.simulate_shared_keystrokes(["f", "o", ";", ",", "i"])
|
||||||
|
.await;
|
||||||
|
cx.assert_shared_state("hellˇo hello\nhello hello").await;
|
||||||
|
cx.assert_shared_mode(Mode::Insert).await;
|
||||||
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub struct NeovimBackedTestContext {
|
||||||
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
// 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.
|
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||||
exemptions: HashMap<String, Option<HashSet<String>>>,
|
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||||
neovim: NeovimConnection,
|
pub(crate) neovim: NeovimConnection,
|
||||||
|
|
||||||
last_set_state: Option<String>,
|
last_set_state: Option<String>,
|
||||||
recent_keystrokes: Vec<String>,
|
recent_keystrokes: Vec<String>,
|
||||||
|
|
|
@ -42,6 +42,7 @@ pub enum NeovimData {
|
||||||
Key(String),
|
Key(String),
|
||||||
Get { state: String, mode: Option<Mode> },
|
Get { state: String, mode: Option<Mode> },
|
||||||
ReadRegister { name: char, value: String },
|
ReadRegister { name: char, value: String },
|
||||||
|
Exec { command: String },
|
||||||
SetOption { value: String },
|
SetOption { value: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +270,32 @@ impl NeovimConnection {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
pub async fn exec(&mut self, value: &str) {
|
||||||
|
self.nvim
|
||||||
|
.command_output(format!("{}", value).as_str())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.data.push_back(NeovimData::Exec {
|
||||||
|
command: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
pub async fn exec(&mut self, value: &str) {
|
||||||
|
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||||
|
self.data.pop_front();
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
self.data.pop_front(),
|
||||||
|
Some(NeovimData::Exec {
|
||||||
|
command: value.to_string(),
|
||||||
|
}),
|
||||||
|
"operation does not match recorded script. re-record with --features=neovim"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "neovim"))]
|
#[cfg(not(feature = "neovim"))]
|
||||||
pub async fn read_register(&mut self, register: char) -> String {
|
pub async fn read_register(&mut self, register: char) -> String {
|
||||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||||
|
|
15
crates/vim/test_data/test_comma_w.json
Normal file
15
crates/vim/test_data/test_comma_w.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{"Exec":{"command":"map ,w j"}}
|
||||||
|
{"Put":{"state":"ˇhello hello\nhello hello"}}
|
||||||
|
{"Key":"f"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":";"}
|
||||||
|
{"Key":","}
|
||||||
|
{"Key":"w"}
|
||||||
|
{"Get":{"state":"hello hello\nhello hellˇo","mode":"Normal"}}
|
||||||
|
{"Put":{"state":"ˇhello hello\nhello hello"}}
|
||||||
|
{"Key":"f"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":";"}
|
||||||
|
{"Key":","}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Get":{"state":"hellˇo hello\nhello hello","mode":"Insert"}}
|
8
crates/vim/test_data/test_jk.json
Normal file
8
crates/vim/test_data/test_jk.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{"Exec":{"command":"imap jk <esc>"}}
|
||||||
|
{"Put":{"state":"ˇhello"}}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"k"}
|
||||||
|
{"Get":{"state":"jˇohello","mode":"Normal"}}
|
Loading…
Reference in a new issue