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:
1 parent
84aefbecdb
commit
c587c42115
37 files changed
+5579
-730
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//! Editing titles: rapid entry, empty titles, `o` inside a title, and the
|
||||
//! document following the text.
|
||||
//! Editing a node's text: rapid entry, empty titles, `o` inside a title,
|
||||
//! bodies and tags, and the document following the text.
|
||||
|
||||
use super::*;
|
||||
use crate::app::buffer::Buffer;
|
||||
@@ -91,7 +91,7 @@ impl App {
|
||||
Err(message) => self.said.push(message.text),
|
||||
}
|
||||
}
|
||||
Action::App(AppAction::LeaveMode) => self.mode = Mode::Normal,
|
||||
Action::App(AppAction::LeaveMode | AppAction::EndEdit) => self.mode = Mode::Normal,
|
||||
Action::App(AppAction::Say(message)) => self.said.push(message.text),
|
||||
Action::App(other) => panic!("unexpected here: {other:?}"),
|
||||
}
|
||||
@@ -204,3 +204,76 @@ fn shift_tab_takes_a_new_idea_out_a_level() {
|
||||
app.press("oMore<S-Tab>");
|
||||
assert_eq!(app.said.last().map(String::as_str), Some("this node is right under the root already"));
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// The body of a node.
|
||||
fn body(&self, id: NodeId) -> &str {
|
||||
match &self.doc().node(id).unwrap().content {
|
||||
Content::Idea(idea) => idea.body.as_str(),
|
||||
Content::Portal(_) => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn tags(&self, id: NodeId) -> String {
|
||||
self.doc().node(id).unwrap().tags.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bodies_take_lines_and_tab() {
|
||||
let (doc, g) = garden();
|
||||
let mut app = App::new(doc, g.beds);
|
||||
app.run(vec![Action::App(AppAction::StartEdit(StartEdit::Body { at: EditAt::End, in_card: false }))]);
|
||||
app.press("Cedar.<CR><Tab>Three of them.");
|
||||
assert_eq!(app.body(g.beds), "Cedar.\n Three of them.", "Enter starts a line, Tab types spaces");
|
||||
// Vim keys work across the lines: k goes up a line, o opens one below.
|
||||
app.press("<Esc>kA!<Esc>oFrom the mill.<Esc><Esc>");
|
||||
assert_eq!(app.body(g.beds), "Cedar.!\nFrom the mill.\n Three of them.");
|
||||
assert_eq!(app.mode.label(), "NORMAL");
|
||||
app.buffer.apply(DocAction::Undo(1));
|
||||
assert_eq!(app.body(g.beds), "", "the whole edit is one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retyping_a_body_clears_every_line() {
|
||||
let (doc, g) = garden();
|
||||
let mut app = App::new(doc, g.compost);
|
||||
app.run(vec![Action::App(AppAction::StartEdit(StartEdit::Body { at: EditAt::Retype, in_card: false }))]);
|
||||
app.press("Ask on Sunday.<Esc><Esc>");
|
||||
assert_eq!(app.body(g.compost), "Ask on Sunday.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tags_follow_the_text_and_bad_ones_come_back() {
|
||||
let (doc, g) = garden();
|
||||
let mut app = App::new(doc, g.soil);
|
||||
app.run(vec![Action::App(AppAction::StartEdit(StartEdit::Tags(EditAt::End)))]);
|
||||
app.press(" #Spring");
|
||||
assert_eq!(app.tags(g.soil), "#spring", "tags are lowercase");
|
||||
app.press("<CR>");
|
||||
assert_eq!(app.mode.label(), "NORMAL", "Enter is done, in tags");
|
||||
app.run(vec![Action::App(AppAction::StartEdit(StartEdit::Tags(EditAt::End)))]);
|
||||
app.press(" #<Esc><Esc>");
|
||||
assert_eq!(app.tags(g.soil), "#spring");
|
||||
assert_eq!(
|
||||
app.said.last().map(String::as_str),
|
||||
Some("a tag can't be empty: the tags are back as they were")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_portal_keeps_its_title_and_body() {
|
||||
let (mut doc, g) = garden();
|
||||
let portal = crate::model::test_support::portal(&mut doc, g.root);
|
||||
let mut app = App::new(doc, portal);
|
||||
app.press("i");
|
||||
assert_eq!(app.mode.label(), "NORMAL");
|
||||
app.run(vec![Action::App(AppAction::StartEdit(StartEdit::Body { at: EditAt::End, in_card: false }))]);
|
||||
assert_eq!(
|
||||
app.said,
|
||||
[
|
||||
"a portal shows its document's name: rename that document instead",
|
||||
"a portal has no body of its own: its document's root has one"
|
||||
]
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user