Add the outline and Note views, splits, popups and the bars

The outline shows the tree as an indented list: a row per node with its
fold marker, tree lines in the parent's color, tags, a ¶ for a body and
+N for what a fold hides. zb shows every body under its title, to read a
document like a page. j/k go row by row, h/l to the parent and the first
child, a click on a marker folds, and titles are edited in their row,
rapid entry included. In move mode, j/k swap the node with its siblings,
h takes it out a level and l puts it under the one above.

The Note view shows one node at full size, as parts: the title, the
tags, the body, each link and each child. j/k step through them, Enter
or i edits the part (or goes to that link or child), and h/l turn the
page to the sibling before or after. Bodies and tags are edited with the
same editor as titles: Enter starts a line in a body, Tab types spaces,
and tags that don't parse come back as they were. cc, C and s now start
typing on an empty line too, as in vim.

Panes split the screen the way vim's windows do (Ctrl-w v s h j k l w c
o =, :split, :vsplit, :only), each in a box with its view and document,
and any pane can show any open document (gt, gT, :b, the documents
picker, a click on the document bar). The status bar has two lines: the
mode, the view, the breadcrumb or a message, the keys typed; then key
hints for what you're doing, which <Space>w hides with which-key.

Popups are a mode of their own, built on one list: ? lists every key and
command, / searches it and Enter runs what you pick; the right-click menu
lists what you can do with a node (with the keys that do the same) or
adds a new idea on the empty canvas; the pickers set a node's color,
border, style or tags. K opens the body in a card beside the node, where
i edits it in place.

The example now opens two documents (the garden, and the kitchen its
portal leads to) and its tests drive all of the above end to end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-01 19:10:09 -04:00
1 parent 84aefbecdb
commit c587c42115
37 files changed
+5579 -730

No files matched your search

+490 -569
View File
File diff suppressed because it is too large. Load diff
+621
View File
@@ -0,0 +1,621 @@
//! The playground driven by typed keys and mouse gestures on a test
//! terminal: what the stages say works, checked end to end.
use super::*;
use cumulus::input::key::parse_keys;
use cumulus::input::keymap::DEFAULT_LEADER;
use cumulus::model::{Border, CanvasPos, Content, NodeId};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
struct Harness {
playground: Playground,
terminal: Terminal<TestBackend>,
}
impl Harness {
fn new() -> Harness {
let terminal = Terminal::new(TestBackend::new(100, 32)).unwrap();
let mut harness = Harness { playground: Playground::new(documents().unwrap()), terminal };
harness.draw();
harness
}
fn draw(&mut self) {
let Harness { playground, terminal } = self;
terminal.draw(|frame| playground.draw(frame)).unwrap();
}
/// Types keys written in vim notation, drawing after each one as the
/// event loop does.
fn type_keys(&mut self, keys: &str) {
for key in parse_keys(keys, DEFAULT_LEADER).unwrap() {
self.playground.key(key);
self.draw();
}
}
fn mouse(&mut self, gesture: Gesture) {
self.playground.gesture(gesture);
self.draw();
}
fn click(&mut self, (col, row): (u16, u16)) {
let at = Cell { col, row };
self.mouse(Gesture::Press { at, ctrl: false });
self.mouse(Gesture::Release { at, dragged: false });
}
/// A double-click, as the mouse reader reports it: a press, then a
/// second one on the same cell.
fn double_click(&mut self, (col, row): (u16, u16)) {
let at = Cell { col, row };
self.click((col, row));
self.mouse(Gesture::DoublePress { at });
self.mouse(Gesture::Release { at, dragged: false });
}
fn right_click(&mut self, (col, row): (u16, u16)) {
self.mouse(Gesture::RightPress { at: Cell { col, row } });
}
/// The focused pane's document.
fn doc(&self) -> &Document {
self.playground.state.buffers[self.playground.buffer()].doc()
}
/// The title of the cursor's node.
fn cursor(&self) -> String {
let buffer = &self.playground.state.buffers[self.playground.buffer()];
self.title(buffer.cursor())
}
fn title(&self, id: NodeId) -> String {
self.doc().node(id).unwrap().title().to_string()
}
fn label(&self) -> &'static str {
self.playground.state.mode.label()
}
/// The text being edited, as typed so far.
fn draft(&self) -> &str {
self.playground.state.mode.any_edit().map_or("", |edit| edit.editor.text())
}
/// Where the text cursor is in the text being edited: a byte offset.
fn draft_cursor(&self) -> Option<usize> {
self.playground.state.mode.any_edit().map(|edit| edit.editor.cursor())
}
fn message(&self) -> &str {
self.playground.message.as_ref().map_or("", |message| &message.text)
}
/// The node with this title.
fn node(&self, title: &str) -> NodeId {
let doc = self.doc();
doc.outline().into_iter().find(|&id| doc.node(id).unwrap().title().as_str() == title).unwrap()
}
fn children(&self, title: &str) -> Vec<String> {
self.doc().children(self.node(title)).iter().map(|&id| self.title(id)).collect()
}
fn parent(&self, title: &str) -> String {
self.title(self.doc().parent(self.node(title)).unwrap())
}
fn pos(&self, title: &str) -> CanvasPos {
self.doc().node(self.node(title)).unwrap().pos
}
fn body(&self, title: &str) -> String {
match &self.doc().node(self.node(title)).unwrap().content {
Content::Idea(idea) => idea.body.as_str().to_string(),
Content::Portal(_) => String::new(),
}
}
fn tags(&self, title: &str) -> String {
self.doc().node(self.node(title)).unwrap().tags.to_string()
}
/// The terminal's cursor, when it shows.
fn text_cursor(&self) -> Option<(u16, u16)> {
let backend = self.terminal.backend();
let at = backend.cursor_position();
backend.cursor_visible().then_some((at.x, at.y))
}
/// What's on screen, row by row.
fn screen(&self) -> String {
self.rows().join("\n")
}
fn rows(&self) -> Vec<String> {
let buffer = self.terminal.backend().buffer();
let rows = buffer.content().chunks(usize::from(buffer.area.width));
rows.map(|row| row.iter().map(|cell| cell.symbol()).collect()).collect()
}
/// The cell where `text` first shows in the panes (between the document
/// bar and the status bar).
fn find(&self, text: &str) -> (u16, u16) {
let rows = self.rows();
let last = rows.len() - 3;
self.find_in(text, 1..=last).unwrap_or_else(|| panic!("{text:?} isn't on screen:\n{}", self.screen()))
}
fn find_in(&self, text: &str, rows: std::ops::RangeInclusive<usize>) -> Option<(u16, u16)> {
let screen = self.rows();
rows.into_iter().find_map(|row| {
let at = screen[row].find(text)?;
let col = screen[row][..at].chars().count();
Some((u16::try_from(col).ok()?, u16::try_from(row).ok()?))
})
}
}
// ----- The canvas and editing (stages 2 to 6) -----------------------------
#[test]
fn marks_counts_and_undo() {
let mut h = Harness::new();
h.type_keys("glma");
assert_eq!(h.cursor(), "Soil");
assert_eq!(h.message(), "mark a is on \"Soil\": 'a comes back here");
h.type_keys("gg'a");
assert_eq!(h.cursor(), "Soil");
h.type_keys("''");
assert_eq!(h.cursor(), "Garden plans", "'' goes back to before the last jump");
h.type_keys("'a2dd");
assert_eq!(h.message(), "deleted 4 nodes · u to undo", "Soil, and Raised beds with its two");
assert_eq!(h.cursor(), "Watering");
assert!(!h.screen().contains("Raised beds"));
h.type_keys("u");
assert_eq!(h.cursor(), "Soil");
assert!(h.screen().contains("Raised beds"));
}
#[test]
fn move_mode_moves_the_branch_as_one_undo_step() {
let mut h = Harness::new();
h.type_keys("glgjM");
assert_eq!(h.cursor(), "Raised beds");
assert_eq!(h.label(), "MOVE");
h.type_keys("3lJ<CR>");
assert_eq!(h.label(), "NORMAL");
assert_eq!(h.pos("Raised beds"), CanvasPos::new(25, 12), "3 right, 5 down");
assert_eq!(h.pos("Cedar, untreated"), CanvasPos::new(46, 10), "its branch came along");
h.type_keys("u");
assert_eq!(h.pos("Raised beds"), CanvasPos::new(22, 7));
assert_eq!(h.pos("Cedar, untreated"), CanvasPos::new(43, 5));
}
#[test]
fn dragging_a_node_moves_its_branch() {
let mut h = Harness::new();
let (col, row) = h.find("Soil");
let (start, end) = (Cell { col, row }, Cell { col: col + 4, row: row + 2 });
h.mouse(Gesture::Press { at: start, ctrl: false });
assert_eq!(h.cursor(), "Soil", "the press selects");
h.mouse(Gesture::Drag { from: start, at: end, ctrl: false });
h.mouse(Gesture::Release { at: end, dragged: true });
assert_eq!(h.pos("Soil"), CanvasPos::new(26, 4));
h.type_keys("u");
assert_eq!(h.pos("Soil"), CanvasPos::new(22, 2));
}
#[test]
fn command_lines_run_through_the_same_actions() {
let mut h = Harness::new();
h.type_keys(":color teal<CR>");
let root = h.doc().node(h.node("Garden plans")).unwrap();
assert_eq!(root.look.color.as_ref().map(|color| color.as_str()), Some("teal"));
h.type_keys(":in mulch<CR>");
assert_eq!(h.message(), "captured \"mulch\" to the Inbox");
assert_eq!(h.parent("mulch"), "Inbox");
assert_eq!(h.cursor(), "Garden plans", "capturing leaves the cursor where it was");
h.type_keys(":find drip<CR>");
assert_eq!(h.cursor(), "Drip line");
assert_eq!(h.message(), "\"drip\": match 1 of 1");
h.type_keys(":set completion_popup=on-tab<CR>");
assert_eq!(h.playground.completion, PopupMode::OnTab);
h.type_keys(":frobnicate<CR>");
assert_eq!(h.playground.message.as_ref().map(|message| message.tone), Some(Tone::Problem));
}
#[test]
fn adding_folding_zooming_and_quitting() {
let mut h = Harness::new();
h.type_keys("aMulch<Esc><Esc>");
assert_eq!(h.cursor(), "Mulch");
h.type_keys("u");
assert_eq!(h.cursor(), "Garden plans", "undo goes back to where the change was");
h.type_keys("glgjza");
assert_eq!(h.cursor(), "Raised beds");
assert!(h.screen().contains("+2"), "a folded branch shows how many nodes it hides");
assert!(!h.screen().contains("40 cm high"));
h.type_keys("-");
assert!(h.screen().contains("canvas · far"), "the status bar shows the zoom");
h.type_keys("q");
assert!(h.playground.quit, "q closes the last pane: that quits");
}
#[test]
fn typing_a_title_in_its_box() {
let mut h = Harness::new();
h.type_keys("gli mix");
assert_eq!(h.label(), "INSERT");
let (col, row) = h.find("Soil mix");
assert_eq!(h.text_cursor(), Some((col + 8, row)), "after the last letter");
assert_eq!(cursor_shape(&h.playground.state.mode), SetCursorStyle::SteadyBar);
h.type_keys("<Esc>");
assert_eq!(h.label(), "TEXT");
let (col, row) = h.find("Soil mix");
assert_eq!(h.text_cursor(), Some((col + 7, row)), "on the last letter, as in vim");
assert_eq!(cursor_shape(&h.playground.state.mode), SetCursorStyle::SteadyBlock);
h.type_keys("<Esc>");
assert_eq!(h.label(), "NORMAL");
assert_eq!(h.text_cursor(), None, "no text cursor outside a title");
assert_eq!(cursor_shape(&h.playground.state.mode), SetCursorStyle::DefaultUserShape);
h.type_keys("u");
assert_eq!(h.cursor(), "Soil", "the whole edit was one undo step");
}
#[test]
fn rapid_entry_with_enter_tab_and_shift_tab() {
let mut h = Harness::new();
h.type_keys("glgjgjaBucket<CR>Hose<Tab>");
assert_eq!(h.children("Watering"), ["Rain barrel", "Drip line", "Bucket"]);
assert_eq!(h.children("Bucket"), ["Hose"], "Tab: under the idea above");
h.type_keys("<CR>Can<S-Tab>");
assert_eq!(
h.children("Watering"),
["Rain barrel", "Drip line", "Bucket", "Can"],
"Shift-Tab: out a level"
);
h.type_keys("<CR><CR>");
assert_eq!(h.label(), "NORMAL", "Enter on an empty idea stops");
assert_eq!(h.children("Watering"), ["Rain barrel", "Drip line", "Bucket", "Can"], "the empty one went");
assert_eq!(h.children("Bucket"), ["Hose"]);
assert_eq!(h.cursor(), "Can");
assert_eq!(h.message(), "");
}
#[test]
fn retyping_and_repeating() {
let mut h = Harness::new();
h.type_keys("glccCompost<Esc><Esc>");
assert_eq!(h.cursor(), "Compost", "cc types the title anew");
h.type_keys("A!<Esc>..");
assert_eq!(h.draft(), "Compost!!!", ". repeats the typing too");
h.type_keys("u<Esc>");
assert_eq!(h.cursor(), "Compost!!", "u inside the title undoes its last change");
h.type_keys("u");
assert_eq!(h.cursor(), "Compost", "u outside undoes the whole edit");
h.type_keys("u");
assert_eq!(h.cursor(), "Soil");
}
#[test]
fn vim_keys_inside_a_title() {
let mut h = Harness::new();
// "Cedar, untreated": change the second word, then find the comma.
h.type_keys("glgjgli<Esc>bcwtreated<Esc>0f,x");
assert_eq!(h.draft(), "Cedar treated");
h.type_keys("w~");
assert_eq!(h.draft(), "Cedar Treated", "~ switches the case and steps right");
h.type_keys("$diw");
assert_eq!(h.draft(), "Cedar ");
h.type_keys("<Esc>");
assert_eq!(h.cursor(), "Cedar", "the title is trimmed");
}
/// Every other vim key inside a title, one step at a time, each checked
/// against what vim does: the text after it, and where the cursor is.
#[test]
fn the_rest_of_the_vim_keys_inside_a_title() {
let mut h = Harness::new();
h.type_keys("glccplant (two) \"red\" beans<Esc>");
let steps: &[(&str, &str, usize)] = &[
("0e", "plant (two) \"red\" beans", 4),
("^", "plant (two) \"red\" beans", 0),
("$F(", "plant (two) \"red\" beans", 6),
("$T\"", "plant (two) \"red\" beans", 17),
// ; repeats the T, skipping the quote it's already next to.
(";", "plant (two) \"red\" beans", 13),
// , repeats it the other way: t".
(",", "plant (two) \"red\" beans", 15),
("0f(ci(six<Esc>", "plant (six) \"red\" beans", 9),
("f\"di\"", "plant (six) \"\" beans", 13),
("da\"", "plant (six) beans", 12),
("F(da(", "plant beans", 6),
("X", "plant beans", 5),
("wdaw", "plant", 4),
("A beans<Esc>", "plant beans", 10),
("0sP<Esc>", "Plant beans", 0),
("$r!", "Plant bean!", 10),
("u", "Plant beans", 10),
("<C-r>", "Plant bean!", 10),
("0wD", "Plant ", 5),
("0CSeed<Esc>", "Seed", 3),
("0yiwP", "SeedSeed", 3),
("I><Esc>", ">SeedSeed", 0),
("a-<Esc>", ">-SeedSeed", 1),
("SGo<Esc>", "Go", 1),
("0iE<Esc>", "EGo", 0),
("3~", "egO", 2),
("Sa.b.c.d.e.f.g<Esc>0d3f.", "d.e.f.g", 0),
("2x", "e.f.g", 0),
("d2f.", "g", 0),
// A title has one line, so Y's line goes in like any text.
("YP", "gg", 0),
];
for &(keys, text, cursor) in steps {
h.type_keys(keys);
assert_eq!((h.draft(), h.draft_cursor()), (text, Some(cursor)), "after {keys}");
}
h.type_keys("<Esc>u");
assert_eq!(h.cursor(), "Soil", "all of it was one undo step");
}
#[test]
fn registers_carry_text_from_title_to_title() {
let mut h = Harness::new();
h.type_keys("gli<Esc>\"ayiw<Esc>");
assert_eq!(h.label(), "NORMAL");
h.type_keys("gjA <Esc>\"ap<Esc>");
assert_eq!(h.cursor(), "Raised beds Soil", "\"a kept Soil");
}
#[test]
fn o_inside_a_title_starts_a_sibling() {
let mut h = Harness::new();
h.type_keys("gli<Esc>oCompost<Esc><Esc>");
assert_eq!(h.children("Garden plans"), ["Soil", "Compost", "Raised beds", "Watering", "Kitchen"]);
}
#[test]
fn the_mouse_while_editing() {
let mut h = Harness::new();
let watering = h.node("Watering");
// A double-click on the first e: typing goes in right before it.
let (col, row) = h.find("Watering");
h.double_click((col + 3, row));
assert_eq!(h.label(), "INSERT");
h.type_keys("-");
assert_eq!(h.draft(), "Wat-ering");
// A click in the title moves the text cursor.
let at = h.find("Wat-ering");
h.click(at);
h.type_keys("~");
assert_eq!(h.draft(), "~Wat-ering");
// A click anywhere else keeps the edit, finishes it, and selects.
let soil = h.find("Soil");
h.click(soil);
assert_eq!(h.label(), "NORMAL");
assert_eq!(h.cursor(), "Soil");
assert_eq!(h.title(watering), "~Wat-ering");
}
#[test]
fn indenting_and_outdenting_nodes() {
let mut h = Harness::new();
h.type_keys("glgj>>");
assert_eq!(h.cursor(), "Raised beds");
assert_eq!(h.parent("Raised beds"), "Soil", ">>: under the node above");
h.type_keys("<<");
assert_eq!(h.children("Garden plans"), ["Soil", "Raised beds", "Watering", "Kitchen"], "<<: back out");
h.type_keys("u");
assert_eq!(h.parent("Raised beds"), "Soil");
h.type_keys("u");
assert_eq!(h.parent("Raised beds"), "Garden plans");
assert_eq!(h.pos("Raised beds"), CanvasPos::new(22, 7), "back where it was");
}
#[test]
fn keys_from_later_stages_say_so() {
let mut h = Harness::new();
h.type_keys("yy");
assert_eq!(h.message(), "\"yank (copy)…\" comes in a later stage of the build");
h.type_keys("v");
assert_eq!(h.message(), "\"select\" comes in a later stage of the build");
// Kitchen is a portal: its title is the other document's name.
h.type_keys("glgjgjgj");
assert_eq!(h.cursor(), "Kitchen");
h.type_keys("i");
assert_eq!(h.message(), "a portal shows its document's name: rename that document instead");
assert_eq!(h.label(), "NORMAL");
h.type_keys("<CR>");
assert_eq!(h.message(), "going through a portal comes in stage 8");
}
// ----- Views, panes, popups and documents (stage 7) -----------------------
#[test]
fn panes_split_and_share_the_cursor() {
let mut h = Harness::new();
h.type_keys("<C-w>v<Space>vo");
assert_eq!(h.playground.panes.count(), 2);
assert!(h.screen().contains("outline · Garden plans"), "the new pane shows the outline:\n{}", h.screen());
assert!(h.screen().contains("▾ Garden plans"));
h.type_keys("j");
assert_eq!(h.cursor(), "Soil", "j in the outline: the next row");
h.type_keys("<C-w>l");
assert_eq!(h.playground.panes.current().view.kind(), ViewKind::Canvas);
assert_eq!(h.cursor(), "Soil", "panes on one document share its cursor");
h.type_keys(":vsplit note<CR>");
assert_eq!(h.playground.panes.count(), 3);
assert!(h.screen().contains("note · Garden plans"));
h.type_keys("<C-w>o");
assert_eq!(h.playground.panes.count(), 1, "only this pane");
h.type_keys("<C-w>s");
assert_eq!(h.playground.panes.count(), 2);
h.type_keys("<C-w>c");
assert_eq!(h.playground.panes.count(), 1);
h.type_keys("<C-w>j");
assert_eq!(h.message(), "no pane that way");
}
#[test]
fn the_outline_edits_reorders_and_reads() {
let mut h = Harness::new();
h.type_keys("<Space>vo");
assert!(h.screen().contains("▾ Garden plans"));
h.type_keys("jA!<Esc><Esc>");
assert_eq!(h.cursor(), "Soil!");
assert!(h.screen().contains("├ Soil!"));
// Move mode in the outline: j swaps the node with the sibling below.
h.type_keys("Mj<CR>");
assert_eq!(h.children("Garden plans")[..2], ["Raised beds", "Soil!"]);
h.type_keys("u");
assert_eq!(h.children("Garden plans")[0], "Soil!");
// zb: every body under its title.
assert!(!h.screen().contains("The neighbor has a bin."));
h.type_keys("zb");
assert!(h.screen().contains("The neighbor has a bin."));
}
#[test]
fn the_note_view_edits_its_parts() {
let mut h = Harness::new();
h.type_keys("<Space>vngl");
assert!(h.screen().contains("note · Garden plans"));
assert!(h.screen().contains("#todo"), "Soil's tags");
// j j: the body; Enter: write it.
h.type_keys("jj<CR>Loamy, with compost.<CR>Test the pH.<Esc><Esc>");
assert_eq!(h.body("Soil"), "Loamy, with compost.\nTest the pH.");
assert!(h.screen().contains("Test the pH."));
// k: the tags; Enter: add one.
h.type_keys("k<CR> #spring<CR>");
assert_eq!(h.tags("Soil"), "#spring #todo");
h.type_keys("u");
assert_eq!(h.tags("Soil"), "#todo");
// On Raised beds, the children are parts too: Enter goes there.
h.type_keys("gjjjj<CR>");
assert_eq!(h.cursor(), "Cedar, untreated");
}
#[test]
fn help_finds_and_runs_keys_and_commands() {
let mut h = Harness::new();
h.type_keys("?");
assert_eq!(h.label(), "HELP");
assert!(h.screen().contains("help · canvas"));
h.type_keys("/zoom out<CR>");
assert_eq!(h.label(), "NORMAL");
assert!(h.screen().contains("canvas · far"), "the help ran it");
h.type_keys("?/:color<CR>teal<CR>");
let root = h.doc().node(h.node("Garden plans")).unwrap();
assert_eq!(
root.look.color.as_ref().map(|color| color.as_str()),
Some("teal"),
":color waited for the color"
);
}
#[test]
fn the_card_shows_and_edits_the_body() {
let mut h = Harness::new();
h.type_keys(":find compost<CR>K");
assert_eq!(h.label(), "CARD");
assert!(h.screen().contains("The neighbor has a bin."));
h.type_keys("i");
assert_eq!(h.label(), "INSERT");
assert_eq!(cursor_shape(&h.playground.state.mode), SetCursorStyle::SteadyBar);
h.type_keys("<CR>Gloves too.<Esc><Esc>");
assert_eq!(h.label(), "CARD", "done editing: the card stays");
assert_eq!(h.body("ask about compost"), "The neighbor has a bin.\nBring a bucket.\nGloves too.");
assert!(h.screen().contains("Gloves too."));
h.type_keys("K");
assert_eq!(h.label(), "NORMAL");
h.type_keys("u");
assert_eq!(h.body("ask about compost"), "The neighbor has a bin.\nBring a bucket.");
}
#[test]
fn the_menu_on_a_node_and_on_the_empty_canvas() {
let mut h = Harness::new();
let soil = h.find("Soil");
h.right_click(soil);
assert_eq!((h.label(), h.cursor().as_str()), ("MENU", "Soil"));
assert!(h.screen().contains("add a child"), "{}", h.screen());
h.type_keys("/color<CR>");
assert_eq!(h.label(), "PICK", "color… opens the color picker");
h.type_keys("/teal<CR>");
let node = h.doc().node(h.node("Soil")).unwrap();
assert_eq!(node.look.color.as_ref().map(|color| color.as_str()), Some("teal"));
// On the empty canvas: a new idea right there.
h.right_click((95, 10));
h.type_keys("<CR>Mulch<Esc><Esc>");
let mulch = h.node("Mulch");
assert!(h.doc().loose().contains(&mulch), "a loose idea");
}
#[test]
fn pickers_and_documents() {
let mut h = Harness::new();
h.type_keys("gl<Space>B/dashed<CR>");
assert_eq!(h.doc().node(h.node("Soil")).unwrap().look.border, Some(Border::Dashed));
// #todo is the only tag: Enter takes it off; the picker stays open.
h.type_keys("<Space>t<CR>");
assert_eq!((h.label(), h.tags("Soil").as_str()), ("PICK", ""));
h.type_keys("<Esc>");
// The documents: gt, the picker, :b, and a click on the bar.
h.type_keys("gt");
assert!(h.screen().contains("canvas · Kitchen"));
assert_eq!(h.cursor(), "Kitchen");
h.type_keys("<Space>bk<CR>");
assert_eq!(h.cursor(), "Soil", "back in the garden, where the cursor was");
h.type_keys(":b kitchen<CR>");
assert_eq!(h.cursor(), "Kitchen");
let garden = h.find_in("Garden plans", 0..=0).unwrap();
h.click(garden);
assert_eq!(h.cursor(), "Soil");
}
#[test]
fn more_keys_across_views() {
let mut h = Harness::new();
// Rapid entry in the outline: o adds a row, Enter the next one.
h.type_keys("<Space>vojoMulch<CR>Straw<Esc><Esc>");
assert_eq!(h.children("Garden plans")[..3], ["Soil", "Mulch", "Straw"]);
assert!(h.screen().contains("├ Straw"));
// za folds in the outline as anywhere.
h.type_keys("ggza");
assert!(h.screen().contains("▸ Garden plans"), "{}", h.screen());
h.type_keys("za");
// The Note view: h and l turn the page, K opens the card.
h.type_keys("<Space>vngll");
assert_eq!(h.cursor(), "Mulch", "l: the sibling after Soil");
h.type_keys("h");
assert_eq!(h.cursor(), "Soil");
h.type_keys("K");
assert_eq!(h.label(), "CARD");
h.type_keys("<Esc>");
// Ctrl-w w goes round the panes; Ctrl-w = evens them out.
h.type_keys("<C-w>v");
let first = h.playground.panes.focused();
h.type_keys("<C-w>w");
assert_ne!(h.playground.panes.focused(), first);
h.type_keys("<C-w>=<C-w>o");
// gT: the document before, round to the last.
h.type_keys("gT");
assert_eq!(h.cursor(), "Kitchen");
h.type_keys("gT");
// :help with a topic opens it searched.
h.type_keys(":help fold<CR>");
assert_eq!(h.label(), "HELP");
assert!(h.screen().contains("/fold"));
h.type_keys("<Esc><Esc>");
// <Space>s: this theme has no named styles.
h.type_keys("<Space>s");
assert_eq!(h.message(), "this theme has no named styles yet: theme files come in stage 8");
// <Space>w: no key hints: which-key and the hint line go.
assert!(h.rows().last().unwrap().contains("? help"));
h.type_keys("<Space>w");
assert_eq!(h.message(), "key hints off");
assert!(!h.screen().contains("? help"), "{}", h.screen());
}
+43
View File
@@ -0,0 +1,43 @@
// cumulus document. Hand edits are fine: cumulus reloads it when it changes.
(
format: 1,
uid: "c01d5e7aa0f3b912",
root: 1,
nodes: [
(
id: 1,
title: "Kitchen",
pos: (0, 6),
color: "yellow",
),
(
id: 2,
parent: 1,
title: "Herbs on the sill",
pos: (16, 2),
color: "green",
),
(
id: 3,
parent: 2,
title: "Basil",
pos: (40, 1),
),
(
id: 4,
parent: 2,
title: "Mint, in its own pot",
pos: (40, 4),
body: "It spreads everywhere if it can.",
),
(
id: 5,
parent: 1,
title: "Compost bin",
pos: (16, 9),
tags: "#todo",
body: "Under the sink.
Empty it on Sundays.",
),
],
)