fix: preserve selector text input and close on escape
This commit is contained in:
@@ -36,7 +36,7 @@ Vamp starts in Normal mode. Press `i`, type text, then Esc to return to Normal.
|
|||||||
| Paste in Normal | `p`/`P` after/before the caret or row for linewise fragments; system clipboard by default |
|
| Paste in Normal | `p`/`P` after/before the caret or row for linewise fragments; system clipboard by default |
|
||||||
| Command palette | Ctrl+p; type to filter, arrows select, Enter accepts, Esc cancels |
|
| Command palette | Ctrl+p; type to filter, arrows select, Enter accepts, Esc cancels |
|
||||||
| Floating/docked explorer in Normal or Visual | Space then `o` / Space then `e` |
|
| Floating/docked explorer in Normal or Visual | Space then `o` / Space then `e` |
|
||||||
| Repository diff in Normal | `:diff`; choose Before then After, Tab changes category, type to search; Working checkout After is editable; `:diff.close` exits |
|
| Repository diff in Normal | `:diff`; choose Before then After, Tab changes category, type to search; Esc closes the selector; Working checkout After is editable; `:diff.close` exits the comparison |
|
||||||
|
|
||||||
Space is the default leader: press its suffix keys in order, not simultaneously. The [editing guide](docs/editing.md) contains the complete key table and behavior reference.
|
Space is the default leader: press its suffix keys in order, not simultaneously. The [editing guide](docs/editing.md) contains the complete key table and behavior reference.
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
- [x] diff view: add character-wise change highlight (enabled by default).
|
- [x] diff view: add character-wise change highlight (enabled by default).
|
||||||
- [x] add tabs: tab support, editor action to open a buffer in a new tab, default tab navigation is `ctrl+option+h/l`, close tab action.
|
- [x] add tabs: tab support, editor action to open a buffer in a new tab, default tab navigation is `ctrl+option+h/l`, close tab action.
|
||||||
- per-tab pane layouts, views, explorers, and comparisons; shared buffers, tab bar, and guarded close/force-close actions.
|
- per-tab pane layouts, views, explorers, and comparisons; shared buffers, tab bar, and guarded close/force-close actions.
|
||||||
- [ ] make diff selector close on escape.
|
- [x] make diff selector close on escape.
|
||||||
- [ ] `:` currently overrides the diff selector input (and maybe other input fields) and spawns the command input on top. this is of course not intended behavior and should be fixed.
|
- [x] `:` currently overrides the diff selector input (and maybe other input fields) and spawns the command input on top. this is of course not intended behavior and should be fixed.
|
||||||
- [ ] diff view: lsp actions should act as they normally would. if an lsp action would open another file, then the file should be opened in another tab outside the diff view - if the referenced file/symbol is part of the changes in the diff, it should just be opened in the diff view.
|
- [ ] diff view: lsp actions should act as they normally would. if an lsp action would open another file, then the file should be opened in another tab outside the diff view - if the referenced file/symbol is part of the changes in the diff, it should just be opened in the diff view.
|
||||||
- this will require tab support.
|
- this will require tab support.
|
||||||
- [ ] diff view: change diff view modes; choose between split and unified diff view (split by default).
|
- [ ] diff view: change diff view modes; choose between split and unified diff view (split by default).
|
||||||
|
|||||||
@@ -199,7 +199,9 @@ config.bind "ctrl+h", nil, mode: config.modes.tree
|
|||||||
|
|
||||||
The diff's changed-file explorer and floating picker use these same tree and selection contexts, including remaps and disabled keys. Enter opens the selected file's comparison; Working checkout After reuses its editable buffer. The catalog contains only changed paths, with A/M/D/R status markers, rather than a disk scan.
|
The diff's changed-file explorer and floating picker use these same tree and selection contexts, including remaps and disabled keys. Enter opens the selected file's comparison; Working checkout After reuses its editable buffer. The catalog contains only changed paths, with A/M/D/R status markers, rather than a disk scan.
|
||||||
|
|
||||||
Ordinary `config.core.file_picker` and `config.core.file_explorer` bindings work inside comparisons. Ctrl+w closes a focused picker/explorer, retaining its query, selection, and expansion state; the panes use the reclaimed width. Ctrl+e has no reserved diff behavior. Close the comparison explicitly with `:diff.close`, its palette action, or the generated action value below. `config.core.diff` remains the opening action and also exposes its immutable `close` child; Escape never closes a comparison.
|
Ordinary `config.core.file_picker` and `config.core.file_explorer` bindings work inside comparisons. Ctrl+w closes a focused picker/explorer, retaining its query, selection, and expansion state; the panes use the reclaimed width. Ctrl+e has no reserved diff behavior. Escape closes the revision selector in either stage, but an opened comparison still closes explicitly through `:diff.close`, its palette action, or the generated action value below. `config.core.diff` remains the opening action and also exposes its immutable `close` child.
|
||||||
|
|
||||||
|
Focused selector/filter input receives printable text before Normal-mode action bindings, so `:` stays query text instead of opening Command input. Explicit tree/selection navigation overrides remain effective. Control-key editor action bindings and Ctrl+p remain available.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
config.bind "leader d q", config.core.diff.close
|
config.bind "leader d q", config.core.diff.close
|
||||||
|
|||||||
+4
-2
@@ -295,7 +295,9 @@ The floating selector has **Revisions / Bookmarks** tabs for jj, or **Commits /
|
|||||||
| Up / Down, Ctrl+k / Ctrl+j | Previous / next entry |
|
| Up / Down, Ctrl+k / Ctrl+j | Previous / next entry |
|
||||||
| Backspace / Ctrl+u | Remove a query grapheme / clear query |
|
| Backspace / Ctrl+u | Remove a query grapheme / clear query |
|
||||||
| Enter | Choose the entry |
|
| Enter | Choose the entry |
|
||||||
| Esc | Cancel loading; After → return to Before; Before → clear query without closing |
|
| Esc | Close either selector stage, cancel outstanding work, and restore the source buffer without discarding edits |
|
||||||
|
|
||||||
|
Printable input belongs to the focused query, including `:`, `_`, and `|`; Normal-mode command or pane bindings cannot steal it. Explicit selection-navigation bindings still take priority. Use Ctrl+p to select an action, or configure a control-key action binding, when you need an editor action while the selector is focused.
|
||||||
|
|
||||||
The selector initially lists local revisions and known tracking refs **without contacting remotes**. **Load refs from origin** (or another configured remote) in Bookmarks/Branches/Tags lists advertised references, including refs never fetched locally, without advancing the selection stage. Selecting a reference such as `some-bookmark@origin` reuses available content or fetches the missing selected reference into a temporary comparison repository. **Load history from origin** in Revisions/Commits explicitly imports searchable remote history. Network operations use the repository's existing credentials; errors stay in the selector. They do not update active refs, snapshot jj's working copy, or change the checkout.
|
The selector initially lists local revisions and known tracking refs **without contacting remotes**. **Load refs from origin** (or another configured remote) in Bookmarks/Branches/Tags lists advertised references, including refs never fetched locally, without advancing the selection stage. Selecting a reference such as `some-bookmark@origin` reuses available content or fetches the missing selected reference into a temporary comparison repository. **Load history from origin** in Revisions/Commits explicitly imports searchable remote history. Network operations use the repository's existing credentials; errors stay in the selector. They do not update active refs, snapshot jj's working copy, or change the checkout.
|
||||||
|
|
||||||
@@ -331,7 +333,7 @@ Diff backgrounds blend the terminal's default background with its red/green pale
|
|||||||
|
|
||||||
Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.
|
Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.
|
||||||
|
|
||||||
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; **it never closes the diff**. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
|
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. While a comparison is open, Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; it leaves the comparison open. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
|
||||||
|
|
||||||
Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens; live After tokens must match the displayed text revision. Missing or invalidated tokens leave Tree-sitter syntax colors intact. Semantic colors and modifiers preserve the change backgrounds.
|
Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens; live After tokens must match the displayed text revision. Missing or invalidated tokens leave Tree-sitter syntax colors intact. Semantic colors and modifiers preserve the change backgrounds.
|
||||||
|
|
||||||
|
|||||||
+59
-63
@@ -164,12 +164,6 @@ pub enum DisplayRow {
|
|||||||
Fold { start: usize, end: usize },
|
Fold { start: usize, end: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct SelectorPosition {
|
|
||||||
category: usize,
|
|
||||||
query: String,
|
|
||||||
selected: RevisionTarget,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum RequestKind {
|
enum RequestKind {
|
||||||
Catalog,
|
Catalog,
|
||||||
@@ -214,7 +208,6 @@ pub struct DiffState {
|
|||||||
request_kind: Option<RequestKind>,
|
request_kind: Option<RequestKind>,
|
||||||
visible: Vec<usize>,
|
visible: Vec<usize>,
|
||||||
search_labels: Vec<String>,
|
search_labels: Vec<String>,
|
||||||
before_position: Option<SelectorPosition>,
|
|
||||||
max_before: usize,
|
max_before: usize,
|
||||||
max_after: usize,
|
max_after: usize,
|
||||||
after_widths: Vec<usize>,
|
after_widths: Vec<usize>,
|
||||||
@@ -255,7 +248,6 @@ impl DiffState {
|
|||||||
request_kind: Some(RequestKind::Catalog),
|
request_kind: Some(RequestKind::Catalog),
|
||||||
visible: Vec::new(),
|
visible: Vec::new(),
|
||||||
search_labels: Vec::new(),
|
search_labels: Vec::new(),
|
||||||
before_position: None,
|
|
||||||
max_before: 0,
|
max_before: 0,
|
||||||
max_after: 0,
|
max_after: 0,
|
||||||
after_widths: Vec::new(),
|
after_widths: Vec::new(),
|
||||||
@@ -468,44 +460,20 @@ impl DiffState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles modal input; closing is an explicit editor action.
|
/// Handles selector filtering and comparison navigation; the editor owns selector closing.
|
||||||
pub(crate) fn handle(&mut self, input: Input) {
|
pub(crate) fn handle(&mut self, input: Input) {
|
||||||
if input == Input::Escape {
|
if input == Input::Escape {
|
||||||
if self.phase == Phase::View
|
if self.phase == Phase::View
|
||||||
&& self.focus == Focus::Files
|
&& self.focus == Focus::Files
|
||||||
&& let Some(files) = &mut self.files
|
&& let Some(files) = &mut self.files
|
||||||
&& files.searching
|
|
||||||
&& files.view == FileView::Tree
|
|
||||||
{
|
{
|
||||||
|
if files.searching && files.view == FileView::Tree {
|
||||||
files.finish_search();
|
files.finish_search();
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
if self.phase == Phase::View {
|
|
||||||
if self.focus == Focus::Files
|
|
||||||
&& let Some(files) = &mut self.files
|
|
||||||
{
|
|
||||||
files.focused = false;
|
files.focused = false;
|
||||||
self.files_visible = files.presentation == FilePresentation::Docked;
|
self.files_visible = files.presentation == FilePresentation::Docked;
|
||||||
self.focus = self.first_text_focus();
|
self.focus = self.first_text_focus();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.pending = None;
|
|
||||||
self.request_kind = None;
|
|
||||||
self.loading = false;
|
|
||||||
self.error = None;
|
|
||||||
if self.phase == Phase::Before {
|
|
||||||
self.query.clear();
|
|
||||||
self.refilter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.phase = Phase::Before;
|
|
||||||
self.after = None;
|
|
||||||
if let Some(position) = self.before_position.take() {
|
|
||||||
self.category = position.category;
|
|
||||||
self.query = position.query;
|
|
||||||
self.refilter();
|
|
||||||
self.select_target(&position.selected);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -562,11 +530,6 @@ impl DiffState {
|
|||||||
} else if let RevisionTarget::ListRemote(remote) = &revision.target {
|
} else if let RevisionTarget::ListRemote(remote) = &revision.target {
|
||||||
self.queue(Operation::ListRemote(remote.clone()));
|
self.queue(Operation::ListRemote(remote.clone()));
|
||||||
} else if self.phase == Phase::Before {
|
} else if self.phase == Phase::Before {
|
||||||
self.before_position = Some(SelectorPosition {
|
|
||||||
category: self.category,
|
|
||||||
query: self.query.clone(),
|
|
||||||
selected: revision.target.clone(),
|
|
||||||
});
|
|
||||||
self.before = Some(revision);
|
self.before = Some(revision);
|
||||||
self.after = None;
|
self.after = None;
|
||||||
self.phase = Phase::After;
|
self.phase = Phase::After;
|
||||||
@@ -2106,32 +2069,66 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn after_escape_restores_before_query_and_selected_revision_and_cancels_request() {
|
fn selector_escape_closes_each_stage_and_rejects_late_results() {
|
||||||
|
for after in [false, true] {
|
||||||
|
let mut editor = editor();
|
||||||
|
editor
|
||||||
|
.apply(vec![Effect::Replace("unsaved source".into())])
|
||||||
|
.unwrap();
|
||||||
|
let buffer = editor.buffer_id();
|
||||||
|
open(&mut editor);
|
||||||
|
let request = if after {
|
||||||
|
Some(choose_pair(&mut editor))
|
||||||
|
} else {
|
||||||
|
text(&mut editor, "query");
|
||||||
|
None
|
||||||
|
};
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
assert!(editor.diff().is_none());
|
||||||
|
assert_eq!(editor.buffer_id(), buffer);
|
||||||
|
assert_eq!(editor.lines(), ["unsaved source"]);
|
||||||
|
assert!(editor.dirty());
|
||||||
|
if let Some(id) = request {
|
||||||
|
assert!(!editor.apply_diff_result(id, Ok(Outcome::Comparison(comparison()))));
|
||||||
|
assert!(editor.diff().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selector_printable_input_precedes_normal_bindings_but_not_selection_overrides() {
|
||||||
|
for after in [false, true] {
|
||||||
let mut editor = editor();
|
let mut editor = editor();
|
||||||
open(&mut editor);
|
open(&mut editor);
|
||||||
editor.handle(Input::Char('\t'));
|
if after {
|
||||||
text(&mut editor, "topic");
|
|
||||||
editor.handle(Input::Down);
|
|
||||||
editor.handle(Input::Enter);
|
editor.handle(Input::Enter);
|
||||||
|
}
|
||||||
|
editor
|
||||||
|
.bind("normal", "x", Some("core.command".into()))
|
||||||
|
.unwrap();
|
||||||
|
for ch in ":_| x".chars() {
|
||||||
|
editor.handle(Input::Char(ch));
|
||||||
|
assert_eq!(editor.mode(), Mode::Normal);
|
||||||
|
assert_eq!(editor.pane_layout().len(), 1);
|
||||||
|
}
|
||||||
|
assert_eq!(editor.diff().unwrap().query, ":_| x");
|
||||||
|
editor.handle(Input::Ctrl('u'));
|
||||||
|
editor
|
||||||
|
.bind("selection", ":", Some("core.down".into()))
|
||||||
|
.unwrap();
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
assert_eq!(editor.diff().unwrap().selected, 1);
|
||||||
assert!(editor.diff().unwrap().query.is_empty());
|
assert!(editor.diff().unwrap().query.is_empty());
|
||||||
editor.handle(Input::Enter);
|
}
|
||||||
let (id, _) = editor.take_diff_request().unwrap();
|
let mut editor = editor();
|
||||||
|
open(&mut editor);
|
||||||
|
editor.handle(Input::Ctrl('p'));
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
assert_eq!(editor.palette().unwrap().query, ":");
|
||||||
|
assert_eq!(editor.mode(), Mode::Normal);
|
||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
let state = editor.diff().unwrap();
|
assert!(editor.palette().is_none());
|
||||||
assert_eq!(state.phase, Phase::Before);
|
assert!(editor.diff().is_some());
|
||||||
assert_eq!(state.category, 1);
|
|
||||||
assert_eq!(state.query, "topic");
|
|
||||||
assert_eq!(state.selected, 1);
|
|
||||||
assert!(!state.loading);
|
|
||||||
assert!(!editor.apply_diff_result(id, Ok(Outcome::Comparison(comparison()))));
|
|
||||||
editor.handle(Input::Enter);
|
|
||||||
assert_eq!(editor.diff().unwrap().phase, Phase::After);
|
|
||||||
editor.handle(Input::Escape);
|
|
||||||
editor.handle(Input::Escape);
|
|
||||||
assert_eq!(editor.diff().unwrap().phase, Phase::Before);
|
|
||||||
assert!(editor.diff().unwrap().query.is_empty());
|
|
||||||
editor.execute("diff.close");
|
|
||||||
assert!(editor.diff().is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2560,8 +2557,7 @@ mod tests {
|
|||||||
buffers
|
buffers
|
||||||
);
|
);
|
||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
assert!(editor.diff().is_some());
|
assert!(editor.diff().is_none());
|
||||||
editor.execute("diff.close");
|
|
||||||
assert_eq!(editor.mode(), Mode::Normal);
|
assert_eq!(editor.mode(), Mode::Normal);
|
||||||
open(&mut editor);
|
open(&mut editor);
|
||||||
let id = choose_pair(&mut editor);
|
let id = choose_pair(&mut editor);
|
||||||
|
|||||||
+103
-42
@@ -2628,6 +2628,32 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_diff(&mut self, input: Input) -> Option<Invocation> {
|
fn handle_diff(&mut self, input: Input) -> Option<Invocation> {
|
||||||
|
if self.palette.is_some()
|
||||||
|
|| self.diff_prompt_mode.is_some()
|
||||||
|
|| self.startup_diagnostic.is_some()
|
||||||
|
|| self.hover.is_some()
|
||||||
|
{
|
||||||
|
return self.handle_input(input);
|
||||||
|
}
|
||||||
|
let phase = self.diff.as_ref().expect("active diff").phase;
|
||||||
|
let files_focused =
|
||||||
|
phase == Phase::View && self.diff.as_ref().unwrap().focus == Focus::Files;
|
||||||
|
if phase != Phase::View && input == Input::Escape {
|
||||||
|
return self.execute_action(Action::DiffClose.entry(), "");
|
||||||
|
}
|
||||||
|
let text_focused = phase != Phase::View
|
||||||
|
|| (files_focused
|
||||||
|
&& self
|
||||||
|
.diff
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|diff| diff.files.as_ref())
|
||||||
|
.is_some_and(|files| files.searching || files.view == FileView::List));
|
||||||
|
if text_focused && matches!(input, Input::Char(ch) if !ch.is_control()) {
|
||||||
|
if !self.handle_selection_binding(&input) {
|
||||||
|
self.diff.as_mut().unwrap().handle(input);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if self
|
if self
|
||||||
.bindings
|
.bindings
|
||||||
.get(&self.binding_mode())
|
.get(&self.binding_mode())
|
||||||
@@ -2655,9 +2681,7 @@ impl Editor {
|
|||||||
{
|
{
|
||||||
return self.handle_input(input);
|
return self.handle_input(input);
|
||||||
}
|
}
|
||||||
if self.palette.is_some()
|
if (self.diff_interacting() && self.active.view.mode != Mode::Normal)
|
||||||
|| self.diff_prompt_mode.is_some()
|
|
||||||
|| (self.diff_interacting() && self.active.view.mode != Mode::Normal)
|
|
||||||
|| self.count.is_some()
|
|| self.count.is_some()
|
||||||
|| self.sequence_pending().is_some()
|
|| self.sequence_pending().is_some()
|
||||||
|| self.g_pending
|
|| self.g_pending
|
||||||
@@ -2666,9 +2690,6 @@ impl Editor {
|
|||||||
{
|
{
|
||||||
return self.handle_input(input);
|
return self.handle_input(input);
|
||||||
}
|
}
|
||||||
let phase = self.diff.as_ref().expect("active diff").phase;
|
|
||||||
let files_focused =
|
|
||||||
phase == Phase::View && self.diff.as_ref().unwrap().focus == Focus::Files;
|
|
||||||
if self.diff_interacting() {
|
if self.diff_interacting() {
|
||||||
if matches!(input, Input::Char('[' | ']')) {
|
if matches!(input, Input::Char('[' | ']')) {
|
||||||
let focus = self.diff_buffer_focus().unwrap();
|
let focus = self.diff_buffer_focus().unwrap();
|
||||||
@@ -2704,29 +2725,7 @@ impl Editor {
|
|||||||
if files_focused && self.handle_tree_binding(&input) {
|
if files_focused && self.handle_tree_binding(&input) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !self.diff_interacting() {
|
if !self.diff_interacting() && self.handle_selection_binding(&input) {
|
||||||
match self.selection_bindings.get(&input) {
|
|
||||||
Some(Binding::Action(Action::Down)) => {
|
|
||||||
self.diff.as_mut().unwrap().navigate(true);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Binding::Action(Action::Up)) => {
|
|
||||||
self.diff.as_mut().unwrap().navigate(false);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Binding::Disabled) => return None,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if files_focused
|
|
||||||
&& matches!(input, Input::Char(ch) if !ch.is_control())
|
|
||||||
&& self
|
|
||||||
.diff
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|diff| diff.files.as_ref())
|
|
||||||
.is_some_and(|files| files.searching || files.view == FileView::List)
|
|
||||||
{
|
|
||||||
self.diff.as_mut().unwrap().handle(input);
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if self.sequence_pending().is_some()
|
if self.sequence_pending().is_some()
|
||||||
@@ -2758,7 +2757,6 @@ impl Editor {
|
|||||||
return self.handle_input(input);
|
return self.handle_input(input);
|
||||||
}
|
}
|
||||||
let escape = input == Input::Escape;
|
let escape = input == Input::Escape;
|
||||||
let back = input == Input::Escape && phase != Phase::View;
|
|
||||||
let return_focus = files_focused
|
let return_focus = files_focused
|
||||||
.then(|| self.diff_bound.map(|bound| bound.2))
|
.then(|| self.diff_bound.map(|bound| bound.2))
|
||||||
.flatten();
|
.flatten();
|
||||||
@@ -2772,9 +2770,6 @@ impl Editor {
|
|||||||
diff.focus = focus;
|
diff.focus = focus;
|
||||||
diff.normalize_focus();
|
diff.normalize_focus();
|
||||||
}
|
}
|
||||||
if back {
|
|
||||||
self.outstanding_diff_request = None;
|
|
||||||
}
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4286,6 +4281,10 @@ impl Editor {
|
|||||||
} else {
|
} else {
|
||||||
palette.selected - 1
|
palette.selected - 1
|
||||||
};
|
};
|
||||||
|
} else if self.palette.is_none()
|
||||||
|
&& let Some(diff) = &mut self.diff
|
||||||
|
{
|
||||||
|
diff.navigate(next);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -8721,6 +8720,70 @@ mod tests {
|
|||||||
assert_eq!(editor.active.view.targets, targets);
|
assert_eq!(editor.active.view.targets, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_text_fields_keep_printable_normal_bindings_as_text() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
fs::write(directory.path("sample.txt"), "text").unwrap();
|
||||||
|
for field in [
|
||||||
|
"floating",
|
||||||
|
"docked",
|
||||||
|
"diff_tree",
|
||||||
|
"diff_list",
|
||||||
|
"palette",
|
||||||
|
"command",
|
||||||
|
"search",
|
||||||
|
] {
|
||||||
|
let mut editor = editor();
|
||||||
|
editor.project_root = directory.0.clone();
|
||||||
|
editor
|
||||||
|
.bind("normal", "x", Some("core.pane.split_vertical".into()))
|
||||||
|
.unwrap();
|
||||||
|
match field {
|
||||||
|
"floating" => editor.open_file_picker(FilePresentation::Floating),
|
||||||
|
"docked" => {
|
||||||
|
editor.open_file_picker(FilePresentation::Docked);
|
||||||
|
editor.handle(Input::Char('i'));
|
||||||
|
}
|
||||||
|
"diff_tree" | "diff_list" => {
|
||||||
|
historical_diff(
|
||||||
|
&mut editor,
|
||||||
|
&directory.0,
|
||||||
|
vec!["before".into()],
|
||||||
|
vec!["after".into()],
|
||||||
|
);
|
||||||
|
if field == "diff_list" {
|
||||||
|
editor.set_file_explorer_view("list").unwrap();
|
||||||
|
}
|
||||||
|
editor.open_file_picker(FilePresentation::Docked);
|
||||||
|
if field == "diff_tree" {
|
||||||
|
editor.handle(Input::Char('i'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"palette" => {
|
||||||
|
editor.handle(Input::Ctrl('p'));
|
||||||
|
}
|
||||||
|
"command" => {
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
}
|
||||||
|
"search" => {
|
||||||
|
editor.handle(Input::Char('/'));
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
let document = editor.lines().to_vec();
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
editor.handle(Input::Char('x'));
|
||||||
|
let query = match field {
|
||||||
|
"palette" => editor.palette().unwrap().query.as_str(),
|
||||||
|
"command" | "search" => editor.command_line(),
|
||||||
|
_ => editor.file_picker().unwrap().query.as_str(),
|
||||||
|
};
|
||||||
|
assert_eq!(query, ":x", "{field}");
|
||||||
|
assert_eq!(editor.pane_layout().len(), 1, "{field}");
|
||||||
|
assert_eq!(editor.lines(), document, "{field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_notification_does_not_swallow_escape_but_cursor_diagnostic_does() {
|
fn empty_notification_does_not_swallow_escape_but_cursor_diagnostic_does() {
|
||||||
let mut editor = editor();
|
let mut editor = editor();
|
||||||
@@ -9637,12 +9700,12 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
editor.handle(input);
|
editor.handle(input);
|
||||||
}
|
}
|
||||||
editor.handle(Input::Escape);
|
|
||||||
assert!(editor.diff().is_some());
|
|
||||||
type_text(&mut editor, ":unknown\n");
|
type_text(&mut editor, ":unknown\n");
|
||||||
editor.handle(Input::Ctrl('p'));
|
editor.handle(Input::Ctrl('p'));
|
||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
editor.execute("diff.close");
|
assert!(editor.diff().is_some());
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
assert!(editor.diff().is_none());
|
||||||
assert_eq!(editor.mode(), Mode::Visual);
|
assert_eq!(editor.mode(), Mode::Visual);
|
||||||
assert_eq!(editor.carets(), carets);
|
assert_eq!(editor.carets(), carets);
|
||||||
assert_eq!(editor.selections(), selections);
|
assert_eq!(editor.selections(), selections);
|
||||||
@@ -9665,14 +9728,12 @@ mod tests {
|
|||||||
let caret = editor.cursor();
|
let caret = editor.cursor();
|
||||||
editor.execute("diff");
|
editor.execute("diff");
|
||||||
editor.handle(Input::Paste("ignored".into()));
|
editor.handle(Input::Paste("ignored".into()));
|
||||||
editor.handle(Input::Escape);
|
|
||||||
assert!(editor.diff().is_some());
|
|
||||||
type_text(&mut editor, ":unfinished");
|
type_text(&mut editor, ":unfinished");
|
||||||
editor.handle(Input::Escape);
|
|
||||||
editor
|
editor
|
||||||
.apply(vec![Effect::Message("message without edits".into())])
|
.apply(vec![Effect::Message("message without edits".into())])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
editor.execute("diff.close");
|
editor.handle(Input::Escape);
|
||||||
|
assert!(editor.diff().is_none());
|
||||||
assert_eq!(editor.mode(), Mode::Insert);
|
assert_eq!(editor.mode(), Mode::Insert);
|
||||||
assert_eq!(editor.cursor(), caret);
|
assert_eq!(editor.cursor(), caret);
|
||||||
type_text(&mut editor, "finish");
|
type_text(&mut editor, "finish");
|
||||||
@@ -9822,7 +9883,7 @@ mod tests {
|
|||||||
editor.handle(Input::Char('q'));
|
editor.handle(Input::Char('q'));
|
||||||
assert_eq!(editor.diff().unwrap().query, "q");
|
assert_eq!(editor.diff().unwrap().query, "q");
|
||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
assert_eq!(editor.diff().unwrap().query, "");
|
assert!(editor.diff().is_none());
|
||||||
assert!(editor.notification_visible());
|
assert!(editor.notification_visible());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10406,7 +10467,7 @@ mod tests {
|
|||||||
editor.execute("diff");
|
editor.execute("diff");
|
||||||
type_text(&mut editor, ":diffclose\n");
|
type_text(&mut editor, ":diffclose\n");
|
||||||
assert!(editor.diff().is_some());
|
assert!(editor.diff().is_some());
|
||||||
type_text(&mut editor, ":diff.close\n");
|
editor.execute("diff.close");
|
||||||
assert!(editor.diff().is_none());
|
assert!(editor.diff().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -51,9 +51,9 @@ Diff: :diff (Normal); choose Before then After; Working checkout After edits liv
|
|||||||
Before and historical/remote After stay read-only; Tab/Shift+Tab categories/Normal focus
|
Before and historical/remote After stay read-only; Tab/Shift+Tab categories/Normal focus
|
||||||
All text panes use native motions, selections and copy; read-only panes reject edits/saves
|
All text panes use native motions, selections and copy; read-only panes reject edits/saves
|
||||||
Switching Before/After preserves the aligned cursor row and display column
|
Switching Before/After preserves the aligned cursor row and display column
|
||||||
Type to search IDs/refs; navigation skips omitted lines without expanding excerpts
|
Selector queries own printable keys, including :; text navigation skips omitted lines
|
||||||
Normal [/] explicitly expands context; Insert brackets remain literal text
|
Normal [/] explicitly expands context; Insert brackets remain literal text
|
||||||
:diff.close exits explicitly; Esc cancels modes/prompts/filtering without closing the diff
|
Esc closes Before/After selectors; :diff.close exits an opened comparison explicitly
|
||||||
Remote refs/history load into temporary comparison repos without changing active refs/checkout
|
Remote refs/history load into temporary comparison repos without changing active refs/checkout
|
||||||
Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths
|
Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -965,7 +965,7 @@ end
|
|||||||
config.bind "D", config.core.buffer_delete
|
config.bind "D", config.core.buffer_delete
|
||||||
config.bind "Z", config.core.buffer_force_delete
|
config.bind "Z", config.core.buffer_force_delete
|
||||||
config.bind "R", config.core.diff
|
config.bind "R", config.core.diff
|
||||||
config.bind "T", config.core.diff.close
|
config.bind "ctrl+t", config.core.diff.close
|
||||||
config.bind "H", config.core.word_wrap.toggle
|
config.bind "H", config.core.word_wrap.toggle
|
||||||
raise "mutable diff actions" unless config.core.frozen? && config.core.diff.frozen? &&
|
raise "mutable diff actions" unless config.core.frozen? && config.core.diff.frozen? &&
|
||||||
config.core.diff.close.frozen? && config.core.diff.name.frozen? &&
|
config.core.diff.close.frozen? && config.core.diff.name.frozen? &&
|
||||||
@@ -982,8 +982,10 @@ end"#,
|
|||||||
assert!(!review.editor().word_wrap());
|
assert!(!review.editor().word_wrap());
|
||||||
review.handle(Input::Char('R'));
|
review.handle(Input::Char('R'));
|
||||||
review.handle(Input::Escape);
|
review.handle(Input::Escape);
|
||||||
|
assert!(review.editor().diff().is_none());
|
||||||
|
review.handle(Input::Char('R'));
|
||||||
assert!(review.editor().diff().is_some());
|
assert!(review.editor().diff().is_some());
|
||||||
review.handle(Input::Char('T'));
|
review.handle(Input::Ctrl('t'));
|
||||||
assert!(review.editor().diff().is_none());
|
assert!(review.editor().diff().is_none());
|
||||||
for (key, dirty, quit, saved) in [
|
for (key, dirty, quit, saved) in [
|
||||||
('Q', false, true, false),
|
('Q', false, true, false),
|
||||||
|
|||||||
+7
-5
@@ -4833,10 +4833,6 @@ mod tests {
|
|||||||
diff.handle(Input::Enter);
|
diff.handle(Input::Enter);
|
||||||
render_diff(&mut terminal, &diff, &mut panels);
|
render_diff(&mut terminal, &diff, &mut panels);
|
||||||
assert!(screen_text(&terminal).contains("Choose After · Before: commit-19"));
|
assert!(screen_text(&terminal).contains("Choose After · Before: commit-19"));
|
||||||
diff.handle(Input::Escape);
|
|
||||||
render_diff(&mut terminal, &diff, &mut panels);
|
|
||||||
assert!(screen_text(&terminal).contains("> needle"));
|
|
||||||
assert!(screen_text(&terminal).contains("> commit-19"));
|
|
||||||
diff.handle(Input::Ctrl('u'));
|
diff.handle(Input::Ctrl('u'));
|
||||||
diff.handle(Input::Char('\t'));
|
diff.handle(Input::Char('\t'));
|
||||||
render_diff(&mut terminal, &diff, &mut panels);
|
render_diff(&mut terminal, &diff, &mut panels);
|
||||||
@@ -5649,7 +5645,6 @@ mod tests {
|
|||||||
fn floating_inputs_share_workspace_geometry_across_documents_splits_and_diffs() {
|
fn floating_inputs_share_workspace_geometry_across_documents_splits_and_diffs() {
|
||||||
for context in ["document", "split", "diff", "split-diff", "selector"] {
|
for context in ["document", "split", "diff", "split-diff", "selector"] {
|
||||||
for prefix in [':', '/'] {
|
for prefix in [':', '/'] {
|
||||||
// A selector's slash belongs to its revision filter, not native search.
|
|
||||||
if context == "selector" && prefix == '/' {
|
if context == "selector" && prefix == '/' {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -5670,7 +5665,14 @@ mod tests {
|
|||||||
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
||||||
}
|
}
|
||||||
editor.set_message("saved notification");
|
editor.set_message("saved notification");
|
||||||
|
if context == "selector" {
|
||||||
|
editor
|
||||||
|
.bind("normal", "ctrl+t", Some("core.command".into()))
|
||||||
|
.unwrap();
|
||||||
|
editor.handle(Input::Ctrl('t'));
|
||||||
|
} else {
|
||||||
editor.handle(Input::Char(prefix));
|
editor.handle(Input::Char(prefix));
|
||||||
|
}
|
||||||
let query = format!("{}TAIL", "界e\u{301}👩🚀".repeat(30));
|
let query = format!("{}TAIL", "界e\u{301}👩🚀".repeat(30));
|
||||||
for ch in query.chars() {
|
for ch in query.chars() {
|
||||||
editor.handle(Input::Char(ch));
|
editor.handle(Input::Char(ch));
|
||||||
|
|||||||
Reference in New Issue
Block a user