feat: navigate command and search input history
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
- [x] make diff comments word wrap / line wrap by default
|
||||
- [x] currently, a review comment seems to be attached even if the user doesn't "save" or write the comment buffer (e.g. using `:w`). just closing it with escape still seems to write it to the db.
|
||||
- comments should be discardable
|
||||
- [ ] cycle previous commands and search text in command and search input when pressing arrows up/down or ctrl+j/k.
|
||||
- [x] cycle previous commands and search text in command and search input when pressing arrows up/down or ctrl+j/k.
|
||||
- [ ] diff view: GitHub code review integration after the local review UX is usable.
|
||||
- publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes.
|
||||
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
|
||||
|
||||
@@ -253,7 +253,7 @@ Normal and Visual Ctrl+u/Ctrl+d invoke `config.core.scroll_step_up` / `config.co
|
||||
| Normal, Visual | `scroll_up`, `scroll_down`, `scroll_step_up`, `scroll_step_down` |
|
||||
| Normal, Insert, Visual | `line_start`, `line_end` |
|
||||
| Normal, Insert, Visual, Tree | `left`, `right` |
|
||||
| Normal, Insert, Visual, Selection, Tree | `down`, `up` |
|
||||
| Normal, Insert, Visual, Command, Search, Selection, Tree | `down`, `up` |
|
||||
| Normal, Insert, Visual | `word_next`, `word_previous`, `word_end`, `big_word_next`, `big_word_previous`, `big_word_end` |
|
||||
| Normal, Insert | `delete` |
|
||||
| Visual | `copy`, `cut`, `change`, `select_lines`, `select_next`, `skip_occurrence`, `select_all` |
|
||||
@@ -341,7 +341,7 @@ config.bind "leader d q", config.core.diff.close
|
||||
|
||||
Ctrl+h is **not** a Backspace alias or default deletion key in any mode. Raw Ctrl+h (0x08) and Backspace (DEL/0x7f) are distinct, as are CSI-u events. In the tree it navigates in NORMAL and INSERT without deleting query text; it never deletes file-buffer Insert text by default. Explicit `config.core.backspace` bindings remain available if desired.
|
||||
|
||||
Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside completion and selection lists, Ctrl+k still clones above in Normal/Visual, and Ctrl+j defaults to newline in Insert or submit in Command/Search. While completion is open, Ctrl+j/k cycles candidates; Enter always closes completion and inserts a newline. Enter and Ctrl+j can be rebound independently outside the completion cycle.
|
||||
Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside completion and selection lists, Ctrl+k still clones above in Normal/Visual, and Ctrl+j inserts a newline in Insert. In Command/Search, Up/Ctrl+k (`config.core.up`) recalls older history and Down/Ctrl+j (`config.core.down`) recalls newer history or restores unfinished input; Enter alone submits by default. These prompt bindings can be overridden independently with `mode: config.modes.command` or `mode: config.modes.search`; they do not use `config.modes.selection`. While completion is open, Ctrl+j/k cycles candidates; Enter always closes completion and inserts a newline. Enter and Ctrl+j can be rebound independently outside the completion cycle.
|
||||
|
||||
## Key sequences and leader
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ Rust tests live under [`tests/`](../tests/). Private unit tests are in [`tests/u
|
||||
`cargo test` runs both groups; `cargo test --lib` and `cargo test --bin vamp` select the library and CLI unit suites. Do not name a unit file `tests/unit/main.rs`: Cargo would discover it as a separate integration target.
|
||||
|
||||
- Editor checks cover Unicode editing, multi-target state, undo/redo, saving failures, and modal paste acceptance and rejection atomicity.
|
||||
- Prompt-history checks cover separate session-wide command/search entries, Unicode draft restoration, editable recalls, clamped/empty/bounded history, duplicate/empty/cancelled submissions, remapped/disabled keys, and preserved Visual/read-only diff targets. Live PTY smoke exercises arrows and raw Ctrl+k/j, restores drafts, accepts a recalled search with Enter, and checks command/search isolation and unchanged source bytes.
|
||||
- Visual-command checks preserve directed characterwise/linewise ranges and multiple carets through command cancellation and plugin dispatch, exercise remapped command entry, and submit `:diff.comment` on either historical side. Live PTY smoke selects lines, cancels a prompt, then runs `:diff.comment` directly and verifies the saved original range without using the palette.
|
||||
- Presentation regressions render retained characterwise/linewise highlights during command entry in ordinary, split-diff, and unified views. Floating-review checks preserve source rectangles and the workspace layout, place the caret inside the overlay, retain explicitly saved text across tab switches, and restore source focus on Escape or pane close.
|
||||
- Review prose checks render long comments and summaries with wrapping enabled by default in split/unified views, preserve exact single-line text and source wrapping, and retain an explicit wrap toggle on reopening. Live PTY smoke displays a long comment across continuation rows, saves it, verifies exact text through the review CLI, and reopens it still wrapped.
|
||||
|
||||
+4
-2
@@ -51,8 +51,8 @@ Indicators refresh asynchronously. Typing and undo/redo retain the last complete
|
||||
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End at every caret outside snippets; Esc returns to Normal, retaining carets |
|
||||
| Insert | Ctrl+n requests completion at the primary caret or cycles next; Ctrl+j/k cycles next/previous while completion is open |
|
||||
| Insert, active snippet | Type to replace the highlighted argument and its mirrors; Tab / Shift+Tab moves next/previous |
|
||||
| Command | Enter executes; Backspace edits; Esc cancels |
|
||||
| Search | Type a literal query; Enter accepts the current occurrence or finds the next match; Backspace edits; Esc cancels |
|
||||
| Command | Enter executes; ↑/↓ or Ctrl+k/j browse history; Backspace edits; Esc cancels |
|
||||
| Search | Type a literal query; Enter accepts the current occurrence or finds the next match; ↑/↓ or Ctrl+k/j browse history; Backspace edits; Esc cancels |
|
||||
| Insert, Command, Search | Ctrl+u deletes backward to the logical line start |
|
||||
| Insert, Command, Search (macOS) | Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start |
|
||||
|
||||
@@ -192,6 +192,8 @@ Submitting an empty search repeats the last query. Esc cancels without moving th
|
||||
|
||||
`:` opens compact floating command input titled **Command**; `/` opens input titled **Search** for buffer text search. Titles sit at the top-left border, replacing the displayed leading `:` / `/`. Both input fields have one column of padding on each side. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Both windows default to the workspace's bottom-left edge, directly above the bottom bar when visible; their [anchors can be configured independently](configuration.md#command-and-search-popup-anchors). They temporarily hide operation notifications and never reserve a permanent prompt/footer row. Tiny windows omit borders/titles or padding when needed to keep input usable.
|
||||
|
||||
Up/Ctrl+k recalls older entries; Down/Ctrl+j recalls newer entries and restores the unfinished input after the newest. Navigation stops at either end without executing a command or moving search targets. Recalled text is editable without changing stored entries. Command and search histories are separate, shared across buffers/panes/tabs, and retained only for this editor session, up to 100 entries each. Enter records nonempty submissions, including unknown commands and searches with no matches; consecutive duplicates are skipped. Empty submissions and cancelled drafts are not recorded. Ctrl+j no longer submits Command/Search input; use Enter.
|
||||
|
||||
Command input works in both Normal and Visual modes, including read-only diff panes. The original selection stays highlighted while typing. Enter dispatches with its ranges, direction, and linewise shape intact; individual commands decide whether to consume or retain them. Escape returns to the original Visual selection without executing. For example, select lines with `V`, then type `:diff.comment` and press Enter—no command palette is needed. `config.core.command` can also be bound in Visual mode.
|
||||
|
||||
The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible, with one column of inner padding on each side when space permits. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar.
|
||||
|
||||
+86
-7
@@ -28,6 +28,7 @@ use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const MAX_EDIT_BYTES: usize = 4 * 1024 * 1024;
|
||||
const PROMPT_HISTORY_LIMIT: usize = 100;
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
|
||||
@@ -667,12 +668,24 @@ core_actions![
|
||||
(
|
||||
"core.down",
|
||||
Action::Down,
|
||||
&[Mode::Normal, Mode::Insert, Mode::Visual],
|
||||
&[
|
||||
Mode::Normal,
|
||||
Mode::Insert,
|
||||
Mode::Visual,
|
||||
Mode::Command,
|
||||
Mode::Search,
|
||||
],
|
||||
),
|
||||
(
|
||||
"core.up",
|
||||
Action::Up,
|
||||
&[Mode::Normal, Mode::Insert, Mode::Visual],
|
||||
&[
|
||||
Mode::Normal,
|
||||
Mode::Insert,
|
||||
Mode::Visual,
|
||||
Mode::Command,
|
||||
Mode::Search,
|
||||
],
|
||||
),
|
||||
(
|
||||
"core.scroll_up",
|
||||
@@ -1552,11 +1565,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
),
|
||||
(
|
||||
Input::Ctrl('j'),
|
||||
if mode == Mode::Insert {
|
||||
Newline
|
||||
} else {
|
||||
Submit
|
||||
},
|
||||
if mode == Mode::Insert { Newline } else { Down },
|
||||
),
|
||||
] {
|
||||
bindings
|
||||
@@ -1575,6 +1584,14 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
.insert(key, Binding::Action(action));
|
||||
}
|
||||
}
|
||||
for mode in [Mode::Command, Mode::Search] {
|
||||
for (key, action) in [(Input::Up, Up), (Input::Ctrl('k'), Up), (Input::Down, Down)] {
|
||||
bindings
|
||||
.entry(mode)
|
||||
.or_default()
|
||||
.insert(key, Binding::Action(action));
|
||||
}
|
||||
}
|
||||
for mode in [
|
||||
Mode::Normal,
|
||||
Mode::Insert,
|
||||
@@ -1734,6 +1751,8 @@ struct ViewState {
|
||||
search_highlight: bool,
|
||||
search_match: Option<SearchMatch>,
|
||||
command_line: String,
|
||||
prompt_history_index: Option<usize>,
|
||||
prompt_draft: String,
|
||||
command_from_visual: bool,
|
||||
caret_reveal_revision: u64,
|
||||
lsp_center: Cell<Option<(u64, u64, Cursor)>>,
|
||||
@@ -1756,6 +1775,8 @@ impl ViewState {
|
||||
search_highlight: false,
|
||||
search_match: None,
|
||||
command_line: String::new(),
|
||||
prompt_history_index: None,
|
||||
prompt_draft: String::new(),
|
||||
command_from_visual: false,
|
||||
caret_reveal_revision: 0,
|
||||
lsp_center: Cell::new(None),
|
||||
@@ -1778,6 +1799,8 @@ impl ViewState {
|
||||
search_highlight: self.search_highlight,
|
||||
search_match: self.search_match,
|
||||
command_line: self.command_line.clone(),
|
||||
prompt_history_index: self.prompt_history_index,
|
||||
prompt_draft: self.prompt_draft.clone(),
|
||||
command_from_visual: self.command_from_visual,
|
||||
caret_reveal_revision: self.caret_reveal_revision,
|
||||
visual_motions: self.visual_motions.clone(),
|
||||
@@ -2154,6 +2177,8 @@ pub struct Editor {
|
||||
// Closed destinations stay until bounded eviction so they cannot shift jump_index.
|
||||
jumps: VecDeque<JumpLocation>,
|
||||
jump_index: usize,
|
||||
command_history: VecDeque<String>,
|
||||
search_history: VecDeque<String>,
|
||||
clipboard: Vec<String>,
|
||||
clipboard_linewise: bool,
|
||||
clipboard_mode: ClipboardMode,
|
||||
@@ -2269,6 +2294,8 @@ impl Editor {
|
||||
); 3],
|
||||
jumps: VecDeque::new(),
|
||||
jump_index: 0,
|
||||
command_history: VecDeque::new(),
|
||||
search_history: VecDeque::new(),
|
||||
clipboard: Vec::new(),
|
||||
clipboard_linewise: false,
|
||||
clipboard_mode: ClipboardMode::System,
|
||||
@@ -7733,6 +7760,8 @@ impl Editor {
|
||||
}
|
||||
self.pane_close_picker_origin = None;
|
||||
self.active.view.command_line = format!("{} ", entry.name);
|
||||
self.active.view.prompt_history_index = None;
|
||||
self.active.view.prompt_draft.clear();
|
||||
self.palette = None;
|
||||
self.active.view.command_from_visual = self.active.view.mode == Mode::Visual;
|
||||
if self.diff.is_some() && !self.diff_interacting() {
|
||||
@@ -8385,9 +8414,39 @@ impl Editor {
|
||||
self.prepare_command();
|
||||
}
|
||||
self.active.view.command_line = format!("{} ", entry.ex.expect("prompt has Ex alias"));
|
||||
self.active.view.prompt_history_index = None;
|
||||
self.active.view.prompt_draft.clear();
|
||||
self.active.view.mode = Mode::Command;
|
||||
}
|
||||
|
||||
fn navigate_prompt_history(&mut self, previous: bool) {
|
||||
let history = if self.active.view.mode == Mode::Search {
|
||||
&self.search_history
|
||||
} else {
|
||||
&self.command_history
|
||||
};
|
||||
if history.is_empty() {
|
||||
return;
|
||||
}
|
||||
let view = &mut self.active.view;
|
||||
let index = match view.prompt_history_index {
|
||||
None if !previous => return,
|
||||
None => {
|
||||
view.prompt_draft = std::mem::take(&mut view.command_line);
|
||||
history.len() - 1
|
||||
}
|
||||
Some(index) if previous => index.saturating_sub(1).min(history.len() - 1),
|
||||
Some(index) if index + 1 < history.len() => index + 1,
|
||||
Some(_) => {
|
||||
view.command_line = std::mem::take(&mut view.prompt_draft);
|
||||
view.prompt_history_index = None;
|
||||
return;
|
||||
}
|
||||
};
|
||||
view.command_line.clone_from(&history[index]);
|
||||
view.prompt_history_index = Some(index);
|
||||
}
|
||||
|
||||
fn act(&mut self, action: Action) -> Option<Invocation> {
|
||||
if (self.active.view.mode != Mode::Command || action == Action::Normal)
|
||||
&& !matches!(
|
||||
@@ -8620,6 +8679,11 @@ impl Editor {
|
||||
self.start_insert(before, EditStart::Insert(action));
|
||||
self.place_insert(action);
|
||||
}
|
||||
Action::Up | Action::Down
|
||||
if matches!(self.active.view.mode, Mode::Command | Mode::Search) =>
|
||||
{
|
||||
self.navigate_prompt_history(action == Action::Up);
|
||||
}
|
||||
Action::Left
|
||||
| Action::Right
|
||||
| Action::Down
|
||||
@@ -8684,6 +8748,8 @@ impl Editor {
|
||||
Mode::Command
|
||||
};
|
||||
self.active.view.command_line.clear();
|
||||
self.active.view.prompt_history_index = None;
|
||||
self.active.view.prompt_draft.clear();
|
||||
}
|
||||
Action::SearchWord | Action::SearchNext | Action::SearchPrevious => {
|
||||
self.seek_search(action, count);
|
||||
@@ -8756,6 +8822,19 @@ impl Editor {
|
||||
Action::Submit => {
|
||||
let command = std::mem::take(&mut self.active.view.command_line);
|
||||
let searching = self.active.view.mode == Mode::Search;
|
||||
let history = if searching {
|
||||
&mut self.search_history
|
||||
} else {
|
||||
&mut self.command_history
|
||||
};
|
||||
if !(command.is_empty() || (!searching && command.trim().is_empty()))
|
||||
&& history.back() != Some(&command)
|
||||
{
|
||||
if history.len() == PROMPT_HISTORY_LIMIT {
|
||||
history.pop_front();
|
||||
}
|
||||
history.push_back(command.clone());
|
||||
}
|
||||
self.active.view.mode = Mode::Normal;
|
||||
if searching {
|
||||
if !command.is_empty() {
|
||||
|
||||
@@ -56,6 +56,7 @@ Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+
|
||||
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
||||
Outside snippets Enter inserts newline, Tab inserts a tab or configured spaces; Esc returns to Normal
|
||||
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
||||
Command/Search: Up/Ctrl+k older history; Down/Ctrl+j newer history or unfinished input; Enter submits
|
||||
Search: Enter accepts current/next literal match, Esc cancels (case-sensitive, wraps)
|
||||
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
|
||||
:save_and_quit [PATH], :force_save_and_quit [PATH], :plugins, :PLUGIN [ARGS]
|
||||
|
||||
+147
-1
@@ -1067,6 +1067,147 @@ fn focused_text_fields_keep_printable_normal_bindings_as_text() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_history_browses_without_executing_and_restores_unicode_drafts() {
|
||||
for prefix in [':', '/'] {
|
||||
let mut editor = editor();
|
||||
editor.register(plugin("fixture", &["greet"])).unwrap();
|
||||
editor
|
||||
.apply(vec![Effect::Replace(
|
||||
"greet first\ngreet 界e\u{301}\nend".into(),
|
||||
)])
|
||||
.unwrap();
|
||||
for text in ["greet first", "greet 界e\u{301}"] {
|
||||
editor.handle(Input::Char(prefix));
|
||||
type_text(&mut editor, text);
|
||||
editor.handle(Input::Enter);
|
||||
}
|
||||
let cursor = editor.cursor();
|
||||
let document = editor.context().text;
|
||||
editor.handle(Input::Char(prefix));
|
||||
type_text(&mut editor, "draft 👩💻");
|
||||
for (key, expected) in [
|
||||
(Input::Down, "draft 👩💻"),
|
||||
(Input::Up, "greet 界e\u{301}"),
|
||||
(Input::Ctrl('k'), "greet first"),
|
||||
(Input::Up, "greet first"),
|
||||
(Input::Down, "greet 界e\u{301}"),
|
||||
] {
|
||||
assert_eq!(editor.handle(key), None);
|
||||
assert_eq!(editor.command_line(), expected);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.context().text, document);
|
||||
}
|
||||
type_text(&mut editor, " edited");
|
||||
editor.handle(Input::Ctrl('j'));
|
||||
assert_eq!(editor.command_line(), "draft 👩💻");
|
||||
editor.handle(Input::Ctrl('k'));
|
||||
assert_eq!(editor.command_line(), "greet 界e\u{301}");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char(prefix));
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Down);
|
||||
assert_eq!(editor.command_line(), "");
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Up);
|
||||
let invocation = editor.handle(Input::Enter);
|
||||
if prefix == ':' {
|
||||
let invocation = invocation.unwrap();
|
||||
assert_eq!(invocation.command, "greet");
|
||||
assert_eq!(invocation.args, "first");
|
||||
} else {
|
||||
assert_eq!(editor.active.view.search_query, "greet first");
|
||||
assert_eq!(editor.cursor().row, 0);
|
||||
}
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_history_is_separate_bounded_and_skips_empty_cancelled_and_consecutive_duplicates() {
|
||||
let mut editor = editor();
|
||||
for prefix in [':', '/'] {
|
||||
editor.handle(Input::Char(prefix));
|
||||
type_text(&mut editor, "draft");
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Down);
|
||||
assert_eq!(editor.command_line(), "draft");
|
||||
editor.handle(Input::Escape);
|
||||
}
|
||||
for command in [":plugins", ":plugins", ":", "/needle", "/needle", "/"] {
|
||||
type_text(&mut editor, command);
|
||||
editor.handle(Input::Enter);
|
||||
}
|
||||
for (prefix, expected) in [(':', "plugins"), ('/', "needle")] {
|
||||
editor.handle(Input::Char(prefix));
|
||||
type_text(&mut editor, "cancelled");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char(prefix));
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Up);
|
||||
assert_eq!(editor.command_line(), expected);
|
||||
editor.handle(Input::Down);
|
||||
assert_eq!(editor.command_line(), "");
|
||||
editor.handle(Input::Escape);
|
||||
}
|
||||
for index in 0..=PROMPT_HISTORY_LIMIT {
|
||||
type_text(&mut editor, &format!("/query {index}"));
|
||||
editor.handle(Input::Enter);
|
||||
}
|
||||
editor.handle(Input::Char('/'));
|
||||
for _ in 0..PROMPT_HISTORY_LIMIT + 2 {
|
||||
editor.handle(Input::Up);
|
||||
}
|
||||
assert_eq!(editor.command_line(), "query 1");
|
||||
for _ in 0..PROMPT_HISTORY_LIMIT {
|
||||
editor.handle(Input::Down);
|
||||
}
|
||||
assert_eq!(editor.command_line(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_history_is_session_wide_remappable_and_preserves_visual_diff_targets() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
type_text(&mut editor, ":plugins\n/needle\n");
|
||||
editor.execute("tab.new");
|
||||
editor
|
||||
.bind("command", "ctrl+b", Some("core.up".into()))
|
||||
.unwrap();
|
||||
editor
|
||||
.bind("search", "ctrl+f", Some("core.down".into()))
|
||||
.unwrap();
|
||||
editor.bind("command", "up", None).unwrap();
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["needle before".into()],
|
||||
vec!["needle after".into()],
|
||||
);
|
||||
editor.handle(Input::Char('v'));
|
||||
let selections = editor.selections();
|
||||
let cursor = editor.cursor();
|
||||
editor.handle(Input::Char(':'));
|
||||
editor.handle(Input::Up);
|
||||
assert_eq!(editor.command_line(), "");
|
||||
editor.handle(Input::Ctrl('b'));
|
||||
assert_eq!(editor.command_line(), "plugins");
|
||||
assert_eq!(editor.selections(), selections);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.mode(), Mode::Visual);
|
||||
assert_eq!(editor.selections(), selections);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('/'));
|
||||
type_text(&mut editor, "draft");
|
||||
editor.handle(Input::Up);
|
||||
assert_eq!(editor.command_line(), "needle");
|
||||
editor.handle(Input::Ctrl('f'));
|
||||
assert_eq!(editor.command_line(), "draft");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_notification_does_not_swallow_escape_but_cursor_diagnostic_does() {
|
||||
let mut editor = editor();
|
||||
@@ -8408,12 +8549,17 @@ fn ctrl_j_is_distinct_from_enter_and_preserves_editor_defaults() {
|
||||
editor.handle(Input::Escape);
|
||||
editor.register(plugin("fixture", &["greet"])).unwrap();
|
||||
type_text(&mut editor, ":greet arguments");
|
||||
let invocation = editor.handle(Input::Ctrl('j')).unwrap();
|
||||
assert_eq!(editor.handle(Input::Ctrl('j')), None);
|
||||
assert_eq!(editor.mode(), Mode::Command);
|
||||
assert_eq!(editor.command_line(), "greet arguments");
|
||||
let invocation = editor.handle(Input::Enter).unwrap();
|
||||
assert_eq!(invocation.command, "greet");
|
||||
assert_eq!(invocation.args, "arguments");
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
type_text(&mut editor, "/two");
|
||||
editor.handle(Input::Ctrl('j'));
|
||||
assert_eq!(editor.mode(), Mode::Search);
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
assert_eq!(editor.active.view.search_query, "two");
|
||||
assert_eq!(editor.cursor().row, 1);
|
||||
|
||||
Reference in New Issue
Block a user