Make marks and counts work, and the completion popup a setting
Marks: the engine read `ma` and `'a`, but the canvas example only said they'd come later. It now sets and jumps to marks, and `''` goes back to where the cursor was before the last jump (a mark, gg, G or :find). Which-key lists `'` after `'`. Counts on operators now mean something on nodes: `3dd` deletes this node and the next two siblings, with their branches, the way vim's `3dd` deletes three lines (Document::siblings_from). Counts before the operator and before the target still multiply, and which-key now says what they add up to: after `2d3`, the `d` entry reads "6 nodes: this one and the next 5 siblings, with their branches". The completion popup can now wait for Tab, like vim's wildmenu: the `completion_popup` setting is "as-you-type" (the default) or "on-tab". The config file comes with the app core; until then the example takes `:set completion_popup=on-tab`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bfc184f5ef
commit
57ba2c275d
6 files changed
+199
-35
No files matched your search
+93
-12
@@ -22,11 +22,12 @@
|
||||
//! Mouse: click selects, dragging a node moves its branch, dragging empty
|
||||
//! space pans, the wheel scrolls.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use cumulus::command::history::History as CommandHistory;
|
||||
use cumulus::command::parse::{Call, Sources, TagChange, parse};
|
||||
use cumulus::command::parse::{Call, SetArg, Sources, TagChange, parse};
|
||||
use cumulus::input::bindings::Context;
|
||||
use cumulus::input::command::{Bound, Command, Motion, Operator, Target, help_for};
|
||||
use cumulus::input::engine::{KeyEngine, Outcome, Typed};
|
||||
@@ -38,7 +39,7 @@ use cumulus::model::{
|
||||
History, Link, LinkKey, Node, NodeId, Spot, Title, Visible,
|
||||
};
|
||||
use cumulus::storage::document_file;
|
||||
use cumulus::ui::command_line::{CommandLine, LineOutcome};
|
||||
use cumulus::ui::command_line::{CommandLine, LineOutcome, PopupMode};
|
||||
use cumulus::ui::theme::Theme;
|
||||
use cumulus::ui::which_key::WhichKey;
|
||||
use cumulus::view::canvas::boxes::{NodeBox, box_rects};
|
||||
@@ -118,6 +119,12 @@ struct Playground {
|
||||
/// The command line, while it's open.
|
||||
command_line: Option<CommandLine>,
|
||||
command_history: CommandHistory,
|
||||
/// When the command line's popup shows (`:set completion_popup=on-tab`).
|
||||
popup: PopupMode,
|
||||
/// Marked nodes (`ma`), and where the cursor was before the last jump
|
||||
/// (for `''`).
|
||||
marks: BTreeMap<char, NodeId>,
|
||||
before_jump: Option<NodeId>,
|
||||
/// The last frame drawn, to match clicks against.
|
||||
scene: Scene,
|
||||
canvas_area: Rect,
|
||||
@@ -144,6 +151,9 @@ impl Playground {
|
||||
drag: None,
|
||||
command_line: None,
|
||||
command_history: CommandHistory::default(),
|
||||
popup: PopupMode::default(),
|
||||
marks: BTreeMap::new(),
|
||||
before_jump: None,
|
||||
scene: Scene::default(),
|
||||
canvas_area: Rect::default(),
|
||||
follow: true,
|
||||
@@ -284,7 +294,24 @@ impl Playground {
|
||||
let steps = i32::try_from(times).unwrap_or(1);
|
||||
match typed.command {
|
||||
Command::Move(motion) => (0..times).for_each(|_| self.motion(motion)),
|
||||
Command::Operate(Operator::Delete, Target::Current | Target::Branch) => self.delete_branch(),
|
||||
Command::Operate(Operator::Delete, Target::Current | Target::Branch) => {
|
||||
self.delete_branches(times)
|
||||
}
|
||||
Command::SetMark(name) => {
|
||||
self.marks.insert(name, self.cursor);
|
||||
let title =
|
||||
self.doc.node(self.cursor).map(|node| node.title().to_string()).unwrap_or_default();
|
||||
self.message = format!("mark {name} is on \"{title}\" ('{name} comes back here)");
|
||||
}
|
||||
Command::JumpToMark('\'' | '`') => match self.before_jump {
|
||||
Some(id) if self.doc.contains(id) => self.leap(Some(id)),
|
||||
_ => self.message = "no jump to go back from yet".into(),
|
||||
},
|
||||
Command::JumpToMark(name) => match self.marks.get(&name) {
|
||||
Some(&id) if self.doc.contains(id) => self.leap(Some(id)),
|
||||
Some(_) => self.message = format!("the node marked {name} is gone"),
|
||||
None => self.message = format!("no mark {name} yet: m{name} sets it"),
|
||||
},
|
||||
Command::Operate(Operator::Delete, Target::Children) => self.delete_children(),
|
||||
Command::DeleteNode => self.delete_node(),
|
||||
Command::AddChild => self.add(Spot::LastChildOf(self.cursor)),
|
||||
@@ -313,7 +340,8 @@ impl Playground {
|
||||
Command::HalfPageUp => self.camera.pan(0, -i32::from(self.canvas_area.height / 2)),
|
||||
Command::MoveMode => self.mode = Mode::Move { whole: true, change: None },
|
||||
Command::CommandLine => {
|
||||
self.command_line = Some(CommandLine::new(&self.sources(), &self.command_history));
|
||||
self.command_line =
|
||||
Some(CommandLine::new(&self.sources(), &self.command_history, self.popup));
|
||||
}
|
||||
Command::ToggleWhichKey => {
|
||||
self.which_key = !self.which_key;
|
||||
@@ -355,7 +383,12 @@ impl Playground {
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.jump(target);
|
||||
// gg and G are jumps, as in vim: '' comes back from them.
|
||||
if matches!(motion, Motion::First | Motion::Last) {
|
||||
self.leap(target);
|
||||
} else {
|
||||
self.jump(target);
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands in move mode: hjkl moves the node or its branch.
|
||||
@@ -447,7 +480,7 @@ impl Playground {
|
||||
colors: self.theme.palette.keys().map(ToString::to_string).collect(),
|
||||
styles: self.theme.node_styles.keys().map(ToString::to_string).collect(),
|
||||
themes: vec!["default".into()],
|
||||
settings: Vec::new(),
|
||||
settings: vec!["completion_popup".into()],
|
||||
tags,
|
||||
folder: std::env::current_dir().unwrap_or_default(),
|
||||
}
|
||||
@@ -507,6 +540,26 @@ impl Playground {
|
||||
}
|
||||
Call::Find(text) => self.find(&text),
|
||||
Call::Version => self.message = cumulus::version(),
|
||||
Call::Set(SetArg::Value(name, value)) if name == "completion_popup" => {
|
||||
match PopupMode::named(&value) {
|
||||
Some(popup) => {
|
||||
self.popup = popup;
|
||||
self.message = format!("completion_popup = {value}");
|
||||
}
|
||||
None => {
|
||||
let values: Vec<&str> = PopupMode::VALUES.iter().map(|(name, _)| *name).collect();
|
||||
self.message = format!("completion_popup can be {}", values.join(" or "));
|
||||
}
|
||||
}
|
||||
}
|
||||
Call::Set(SetArg::Reset(name)) if name == "completion_popup" => {
|
||||
self.popup = PopupMode::default();
|
||||
self.message = "completion_popup = as-you-type (the default)".into();
|
||||
}
|
||||
Call::Set(SetArg::Show(name)) if name == "completion_popup" => {
|
||||
let current = PopupMode::VALUES.iter().find(|(_, mode)| *mode == self.popup);
|
||||
self.message = format!("completion_popup = {}", current.map_or("?", |(name, _)| *name));
|
||||
}
|
||||
Call::Help(_) => {
|
||||
self.message = "the help screen comes in stage 7; Tab completes any command".into()
|
||||
}
|
||||
@@ -559,7 +612,7 @@ impl Playground {
|
||||
let next = matches.iter().copied().find(|&id| order.iter().position(|&o| o == id) > Some(here));
|
||||
match next.or_else(|| matches.first().copied()) {
|
||||
Some(id) => {
|
||||
self.jump(Some(id));
|
||||
self.leap(Some(id));
|
||||
self.message = format!("{} match(es) for \"{text}\"", matches.len());
|
||||
}
|
||||
None => self.message = format!("nothing matches \"{text}\""),
|
||||
@@ -649,6 +702,14 @@ impl Playground {
|
||||
}
|
||||
}
|
||||
|
||||
/// A jump `''` can come back from (to a mark, `gg`, `G`, a search).
|
||||
fn leap(&mut self, target: Option<NodeId>) {
|
||||
if target.is_some() {
|
||||
self.before_jump = Some(self.cursor);
|
||||
}
|
||||
self.jump(target);
|
||||
}
|
||||
|
||||
fn zoom(&mut self, closer: bool) {
|
||||
let zoom = if closer { self.camera.zoom.closer() } else { self.camera.zoom.further() };
|
||||
if let Some(node) = self.doc.node(self.cursor) {
|
||||
@@ -707,12 +768,32 @@ impl Playground {
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_branch(&mut self) {
|
||||
let id = self.cursor;
|
||||
let next = self.doc.parent(id).or_else(|| self.doc.previous_sibling(id));
|
||||
if let Some(removed) = self.edit(|edit| edit.remove_branch(id)) {
|
||||
/// `dd` (or `dab`), with a count: this node and the next siblings, each
|
||||
/// with its branch, as `3dd` deletes three lines in vim.
|
||||
fn delete_branches(&mut self, count: u32) {
|
||||
let branches = self.doc.siblings_from(self.cursor, usize::try_from(count).unwrap_or(1));
|
||||
let Some(&last) = branches.last() else { return };
|
||||
// The cursor goes to the sibling after them, or before them, or up.
|
||||
let next = self
|
||||
.doc
|
||||
.next_sibling(last)
|
||||
.or_else(|| self.doc.previous_sibling(self.cursor))
|
||||
.or_else(|| self.doc.parent(self.cursor));
|
||||
let removed = self.edit(|edit| {
|
||||
let mut removed = 0;
|
||||
for &branch in &branches {
|
||||
removed += edit.remove_branch(branch)?.len();
|
||||
}
|
||||
Ok(removed)
|
||||
});
|
||||
if let Some(removed) = removed {
|
||||
self.cursor = next.unwrap_or(self.doc.root());
|
||||
self.message = format!("deleted {} node(s) · u to undo", removed.len());
|
||||
let what = if branches.len() == 1 {
|
||||
"1 branch".to_string()
|
||||
} else {
|
||||
format!("{} branches", branches.len())
|
||||
};
|
||||
self.message = format!("deleted {what}, {removed} node(s) in all · u to undo");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user