Add visual mode, node registers, search and the jump list (stage 8c)

The rest of vim, on nodes. Visual mode takes in what lies between where
it started and the cursor: on the canvas, every node the rectangle from
one end's box to the other's touches; in the outline and the Note view,
the nodes in between in reading order. V takes whole branches, Ctrl+click
and Ctrl+drag pick with the mouse, and the operators take exactly what's
selected: d, y, > and <, M, and the pickers on every node at once.

Registers now hold text or nodes, so yy, dd and x keep what they take and
p, P and ]p paste it, layout and links included. "+ is the system
clipboard through arboard: nodes go there as Markdown in the user's
format (## headings, a --- tag list, the body), and come back whole while
the clipboard still holds them. clipboard = "unnamedplus" makes plain
yanks use it, as in vim.

. repeats the last change made with keys, with the text typed after it.
/ opens a Telescope-style search: fuzzy matches over titles, tags and
bodies, best first, with the note of the one you're on beside them; n
and N go on with it, and :find searches straight away. Each pane keeps a
jump list for Ctrl-o and Ctrl-i that crosses documents and reopens closed
ones. Capital marks lead into any document and, like the small ones, are
kept in memory.ron. gd follows a cross-link, and :marks, :jumps and
:registers list what they hold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-02 04:12:27 -04:00
1 parent abe3dc1d84
commit 7bec5bf3fc
45 files changed
+3922 -272

No files matched your search

+36 -16
View File
@@ -6,7 +6,7 @@ use ratatui::layout::Position;
use super::action::{Action, AppAction, DocumentChoice, Message};
use super::mode::Mode;
use super::{App, calls, commands};
use super::{App, calls, commands, visual};
use crate::command::parse::{Sources, parse};
use crate::config::settings::Setting;
use crate::config::theme_file;
@@ -21,25 +21,39 @@ impl App {
/// A key, as typed.
pub fn key(&mut self, key: Key) {
self.message = None;
self.record(key);
match self.shared.mode {
Mode::Command(_) => return self.command_line_key(key),
Mode::Text(_) => return self.text_key(key),
Mode::Popup(_) => return self.popup_key(key),
Mode::Normal | Mode::Move(_) => {}
}
match self.keys.press(key, &self.keymap.tables(self.contexts())) {
Outcome::Run(typed) => self.dispatch(typed),
Outcome::Unknown(keys) => self.message = Some(Message::info(format!("{keys} does nothing here"))),
Outcome::Waiting | Outcome::Cancelled => {}
Mode::Command(_) => self.command_line_key(key),
Mode::Text(_) => self.text_key(key),
Mode::Popup(_) => self.popup_key(key),
Mode::Normal | Mode::Visual(_) | Mode::Move(_) => {
match self.keys.press(key, &self.keymap.tables(self.contexts())) {
Outcome::Run(typed) => self.run_typed(typed),
Outcome::Unknown(keys) => {
self.message = Some(Message::info(format!("{keys} does nothing here")));
}
Outcome::Waiting | Outcome::Cancelled => {}
}
}
}
self.end_recording();
}
/// A command: the focused view has the first say, the shared commands
/// get the rest.
fn dispatch(&mut self, typed: Typed) {
/// A command: the focused view has the first say, then visual mode's
/// commands (in visual mode), then the shared commands.
pub(super) fn dispatch(&mut self, typed: Typed) {
let selected = self.selection().unwrap_or_default();
let pane = self.panes.current_mut();
let kind = pane.view.kind();
let ctx = self.shared.ctx(pane.buffer);
let actions = pane.view.command(&typed, &ctx).or_else(|| commands::shared(&typed, &ctx));
let actions = pane
.view
.command(&typed, &ctx)
.or_else(|| match &self.shared.mode {
Mode::Visual(visual) => visual::command(&typed, visual, &selected, kind, &ctx),
_ => None,
})
.or_else(|| commands::shared(&typed, &ctx));
let actions = actions.unwrap_or_else(|| {
let later = format!("\"{}\" comes in a later part of the build", describe(typed.command));
vec![Action::say(Message::info(later))]
@@ -165,6 +179,12 @@ impl App {
self.shared.mode = Mode::Normal;
}
}
// A click without Ctrl ends a selection, as in most apps.
if let (Mode::Visual(_), Gesture::Press { ctrl: false, .. } | Gesture::DoublePress { .. }) =
(&self.shared.mode, gesture)
{
self.shared.mode = Mode::Normal;
}
if let Mode::Popup(_) = self.shared.mode {
let Mode::Popup(mut popup) = std::mem::take(&mut self.shared.mode) else { return };
let actions = popup.mouse(gesture, &self.shared.ctx(self.buffer()));
@@ -195,10 +215,10 @@ impl App {
}
if target != self.panes.focused() {
// Another pane: a text edited in this one is done first, and
// move mode ends.
// move or visual mode ends.
let ctx = self.shared.ctx(self.buffer());
let mut actions = self.shared.mode.edit().map(|edit| edit.finish(&ctx)).unwrap_or_default();
if let Mode::Move(_) = self.shared.mode {
if let Mode::Move(_) | Mode::Visual(_) = self.shared.mode {
actions.push(Action::App(AppAction::LeaveMode));
}
self.update(actions);