Make p paste as children and P as siblings below

After trying part 8c, the user found p and P doing the same thing on the
canvas: both pasted a sibling below the node (P only differed in the
outline's order). Their pick: p pastes under the node, as its last
children, the way a adds a child; P pastes the next siblings, below it,
where o adds one. ]p went away, since p does what it did.

Inside a title p and P still paste after and before the text cursor, as
in vim: they're their own commands (paste-after, paste-before), and the
node ones are paste-as-child and paste-as-sibling.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-02 05:05:00 -04:00
1 parent 7bec5bf3fc
commit bd819409f4
10 files changed
+59 -53

No files matched your search

+6 -7
View File
@@ -145,7 +145,7 @@ pub enum AppAction {
nodes: Vec<NodeId>,
branches: bool,
},
/// Paste what a register holds (`p`, `P`, `]p`): its nodes, or its text
/// Paste what a register holds (`p`, `P`): its nodes, or its text
/// made into nodes, `times` copies.
Paste {
register: Option<Register>,
@@ -256,15 +256,14 @@ pub enum AppAction {
Say(Message),
}
/// Where `p` and its kin paste, from the cursor's node.
/// Where `p` and `P` paste, from the cursor's node (the user's pick after
/// trying part 8c: `p` pastes under the node, as `a` adds).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PasteAt {
/// Its next siblings (`p`).
After,
/// Its siblings before it (`P`).
Before,
/// Its last children (`]p`).
/// Its last children (`p`).
Child,
/// Its next siblings, below it, where `o` adds one (`P`).
Sibling,
}
/// What `:w` and its kin save.
+7 -9
View File
@@ -64,13 +64,12 @@ pub fn shared(typed: &Typed, ctx: &ViewContext<'_>) -> Option<Vec<Action>> {
app(AppAction::Yank { register: typed.register, nodes: children.to_vec(), branches: true })
}
},
Command::PasteAfter => app(AppAction::Paste { register: typed.register, at: PasteAt::After, times }),
Command::PasteBefore => {
app(AppAction::Paste { register: typed.register, at: PasteAt::Before, times })
}
Command::PasteAsChild => {
app(AppAction::Paste { register: typed.register, at: PasteAt::Child, times })
}
Command::PasteAsSibling => {
app(AppAction::Paste { register: typed.register, at: PasteAt::Sibling, times })
}
Command::Operate(Operator::Indent, Target::Current) => indent(ctx, &siblings(ctx, times), None),
Command::Operate(Operator::Outdent, Target::Current) => outdent(ctx, &siblings(ctx, times), None),
Command::ToggleFold => toggle_fold(ctx),
@@ -415,17 +414,16 @@ fn delete_children(ctx: &ViewContext<'_>, register: Option<Register>) -> Vec<Act
]
}
/// `p`, `P` and `]p`: copies of a clip around the cursor's node, placed
/// where a new sibling or child would go, one below the other, each keeping
/// the clip's own layout.
/// `p` and `P`: copies of a clip under the cursor's node or after it,
/// placed where a new child or sibling would go, one below the other, each
/// keeping the clip's own layout.
pub fn paste(ctx: &ViewContext<'_>, clip: Clip, at: PasteAt, times: u32) -> Option<EditOp> {
let (doc, cursor) = (ctx.doc, ctx.cursor);
let bounds = clip.bounds(|node| ctx.box_size(node))?;
let rects = ctx.box_rects();
let (spot, start) = match at {
PasteAt::After => (Spot::After(cursor), below_spot(cursor, bounds.size, &rects)),
PasteAt::Before => (Spot::Before(cursor), below_spot(cursor, bounds.size, &rects)),
PasteAt::Child => (Spot::LastChildOf(cursor), child_spot(doc, cursor, bounds.size, &rects)),
PasteAt::Sibling => (Spot::After(cursor), below_spot(cursor, bounds.size, &rects)),
};
let mut offsets = Vec::new();
let mut pos = start;
+3 -3
View File
@@ -494,8 +494,8 @@ impl Popup {
Popup::Menu(Runs { list, runs })
}
/// `:registers`: what each register holds. Enter pastes it after the
/// cursor's node.
/// `:registers`: what each register holds. Enter pastes it under the
/// cursor's node, as `p` does.
pub fn registers(registers: &[(char, String)], theme: &Theme) -> Popup {
let key = Style::new().fg(theme.accent).add_modifier(Modifier::BOLD);
let muted = Style::new().fg(theme.muted);
@@ -507,7 +507,7 @@ impl Popup {
rows.push(ListRow::new(spans, what));
runs.push(Run::Keys(vec![Key::char('"'), Key::char(name), Key::char('p')]));
}
let footer = "Enter: paste it after the cursor's node · Esc: close";
let footer = "Enter: paste it under the cursor's node · Esc: close";
let list = ListPopup::new("registers", footer, rows, Place::Center { width: CONFLICT_WIDTH });
Popup::Menu(Runs { list, runs })
}
+24 -22
View File
@@ -17,18 +17,18 @@ fn yank_and_paste_nodes() {
// yy on Raised beds: the node and its branch.
h.type_keys("glgjyy");
assert_eq!(h.message(), "yanked 3 nodes");
// p on Watering: the copy goes after it, with its children.
h.type_keys("gjp");
// P on Watering: the copy goes after it, with its children.
h.type_keys("gjP");
assert_eq!(top(&h), ["Soil", "Raised beds", "Watering", "Raised beds", "Kitchen"]);
let copy = h.doc().children(h.node("Garden plans"))[3];
assert_eq!(h.app.current().cursor(), copy, "the cursor goes to what was pasted");
let titles: Vec<String> = h.doc().children(copy).iter().map(|&id| h.title(id)).collect();
assert_eq!(titles, ["Cedar, untreated", "40 cm high"]);
assert_eq!(h.doc().node(copy).unwrap().look.color.as_ref().map(|c| c.as_str()), Some("orange"));
// ]p: as the last children; 2p: two copies.
h.type_keys("gg]p");
// p: as the last children; 2P: two copies.
h.type_keys("ggp");
assert_eq!(top(&h).len(), 6);
h.type_keys("u2p");
h.type_keys("u2P");
assert_eq!(h.message(), "pasted 6 nodes");
assert_eq!(top(&h), ["Soil", "Raised beds", "Watering", "Raised beds", "Kitchen"]);
assert_eq!(h.doc().loose().len(), 4, "after the root: loose nodes, before the two there were");
@@ -39,32 +39,34 @@ fn yank_and_paste_nodes() {
#[test]
fn deletes_keep_what_they_take() {
let mut h = Harness::new();
// dd then p: the branch moves.
// dd then P: the branch moves, after Watering.
h.type_keys("glgjdd");
assert_eq!(h.cursor(), "Watering");
h.type_keys("p");
h.type_keys("P");
assert_eq!(top(&h), ["Soil", "Watering", "Raised beds", "Kitchen"]);
assert_eq!(h.children("Raised beds"), ["Cedar, untreated", "40 cm high"]);
// x keeps just the node: its children stay behind.
h.type_keys("x");
assert_eq!(top(&h), ["Soil", "Watering", "Cedar, untreated", "40 cm high", "Kitchen"]);
h.type_keys("ggP");
assert_eq!(h.message(), "nothing goes before the root");
h.type_keys("glP");
assert_eq!(top(&h)[0], "Raised beds", "P: before Soil");
// p: under the node, as its last child.
h.type_keys("ggglp");
assert_eq!(h.children("Soil"), ["Raised beds"]);
assert!(h.children("Raised beds").is_empty());
// P on the root: a loose node, where o adds one.
h.type_keys("ggP");
assert_eq!((h.cursor().as_str(), h.doc().loose().len()), ("Raised beds", 3));
// Named registers, and the black hole.
h.type_keys("gl\"ayy");
h.type_keys("\"ayy");
assert_eq!(h.message(), "yanked 1 node into \"a");
h.type_keys("gj\"_dd");
h.type_keys("gggl\"_x");
assert_eq!(h.message(), "deleted 1 node · u to undo");
h.type_keys("gg]p");
h.type_keys("ggp");
assert_eq!(
h.children("Garden plans").last().map(String::as_str),
Some("Raised beds"),
"\"_ kept nothing"
);
h.type_keys("gg\"a]p");
h.type_keys("gg\"ap");
assert_eq!(h.children("Garden plans").iter().filter(|title| *title == "Raised beds").count(), 3);
}
@@ -81,7 +83,7 @@ fn the_clipboard_holds_markdown() {
assert_eq!(h.doc().node(pasted).unwrap().look.color.as_ref().map(|c| c.as_str()), Some("orange"));
// Text another app put there: a node per section, tags and body too.
h.app.registers.clipboard().write("## Mulch\n---\n- todo\n---\nStraw or leaves.\n\n### Straw\n").unwrap();
h.type_keys("gg\"+]p");
h.type_keys("gg\"+p");
assert_eq!(h.cursor(), "Mulch");
assert_eq!((h.tags("Mulch").as_str(), h.body("Mulch").as_str()), ("#todo", "Straw or leaves."));
assert_eq!(h.children("Mulch"), ["Straw"]);
@@ -97,9 +99,9 @@ fn text_and_nodes_paste_into_each_other() {
// A word yanked in a title, pasted on nodes: a node.
h.type_keys("gli<Esc>yiw<Esc>ggp");
assert_eq!(h.cursor(), "Soil");
assert_eq!(h.doc().loose().len(), 3, "after the root: a loose node");
assert_eq!(top(&h).last().map(String::as_str), Some("Soil"), "under the root, at the end");
// Nodes pasted into a title: their titles.
h.type_keys("gg]pu");
h.type_keys("ggpu");
h.type_keys("glgjyyggi <Esc>p");
assert_eq!(h.draft(), "Garden plans Raised beds Cedar, untreated 40 cm high");
}
@@ -141,10 +143,10 @@ fn visual_mode_takes_in_a_rectangle() {
h.type_keys("d");
assert_eq!(top(&h), ["Kitchen"]);
assert_eq!(h.message(), "deleted 8 nodes · u to undo");
// What went is in the register: ]p puts it back under the root, where
// What went is in the register: p puts it back under the root, where
// the cursor went.
assert_eq!(h.cursor(), "Garden plans");
h.type_keys("]p");
h.type_keys("p");
assert_eq!(top(&h), ["Kitchen", "Soil", "Raised beds", "Watering"]);
}
@@ -393,9 +395,9 @@ fn registers_list_and_paste() {
let screen = h.screen();
assert!(screen.contains("\"a 1 node") && screen.contains("\"b 3 nodes"), "{screen}");
assert!(screen.contains("\"+ the system clipboard"));
// Enter on "a: pasted after the cursor's node.
// Enter on "a: pasted under the cursor's node.
h.type_keys("j<CR>");
assert_eq!(top(&h), ["Soil", "Raised beds", "Soil", "Watering", "Kitchen"]);
assert_eq!(h.children("Raised beds"), ["Cedar, untreated", "40 cm high", "Soil"]);
}
#[test]
+1 -1
View File
@@ -1,5 +1,5 @@
//! Nodes into registers and back: `yy` and the deletes keep nodes in a
//! register, `p` `P` `]p` paste them (or a register's text, made into
//! register, `p` and `P` paste them (or a register's text, made into
//! nodes). See [`registers`](super::registers).
use std::collections::BTreeSet;