Tidy a branch and line nodes up (stage 9a)
The first small part of stage 9 (v1.1). <Space>= then m, c or g (the user's pick: leader menus), or :tidy map|chart|grid, lays out the cursor's branch while its top node stays put: a mind map puts each node in the middle of its children (the user's pick), an org chart puts them side by side below, and a grid makes a block of cards beside the node (the user's pick), a card's own branch going along as a mind map. Every node of the branch gets a place, folded away or not (the user's pick), as one undo step, and the message says when the branch now covers nodes outside it. The lines follow the layouts: a parent whose children are all below it sends its branches down to a shared bend, and a new child of it goes beside the last one; a grid card behind another isn't joined by a line, which would look like it joins the two cards. In visual mode, <Space>a then h, l, k, j or c lines the selection up on the box around it, each node bringing its branch along, and : opens the command line on the selection: :align and :tidy take it, and so do :color, :border, :style, :tag and :fold, as one undo step. The layouts are pure functions in view::canvas::arrange, applied as one EditOp::Reposition. Layout and Side moved from the parser to the model so keys can name them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
95c6c3ca37
commit
13a603837e
25 files changed
+709
-65
No files matched your search
+32
-14
@@ -2,21 +2,26 @@
|
||||
//! actions, the way views turn keys into actions.
|
||||
|
||||
use super::action::{Action, AppAction, CursorAfter, DocumentChoice, Message, SaveWhich};
|
||||
use super::commands::set_fold;
|
||||
use super::commands::{self, set_fold};
|
||||
use super::panes::Arrange;
|
||||
use super::popup::PickKind;
|
||||
use super::visual;
|
||||
use crate::command::parse::{Call, SetArg, TagChange};
|
||||
use crate::model::{CanvasPos, CanvasRect, EditOp, Inbox, Link, LinkKey, Node, NodeId, Title};
|
||||
use crate::model::{CanvasPos, CanvasRect, EditOp, Group, Inbox, Link, LinkKey, Node, NodeId, Title};
|
||||
use crate::view::canvas::placement::{child_spot, first_child_spot, free_spot};
|
||||
use crate::view::{ViewContext, ViewKind};
|
||||
|
||||
/// The title of the loose node `:in` collects ideas under.
|
||||
const INBOX: &str = "Inbox";
|
||||
|
||||
/// The actions a command line asks for. Commands from later stages of the
|
||||
/// build say so.
|
||||
pub fn actions_for(call: Call, ctx: &ViewContext<'_>) -> Vec<Action> {
|
||||
/// The actions a command line asks for. A line typed in visual mode acts
|
||||
/// on the nodes that were selected (`on`), as the pickers do: `:color`,
|
||||
/// `:border`, `:style`, `:tag` and `:fold` set each of them (one undo step),
|
||||
/// `:tidy` lays out each branch and `:align` lines them up. Otherwise they
|
||||
/// act on the cursor's node. Commands from later stages of the build say so.
|
||||
pub fn actions_for(call: Call, ctx: &ViewContext<'_>, on: &[NodeId]) -> Vec<Action> {
|
||||
let id = ctx.cursor;
|
||||
let nodes = if on.is_empty() { vec![id] } else { on.to_vec() };
|
||||
match call {
|
||||
// :q closes the pane, as in vim: the last one quits.
|
||||
Call::Quit { force } => app(AppAction::ClosePane { force }),
|
||||
@@ -39,11 +44,15 @@ pub fn actions_for(call: Call, ctx: &ViewContext<'_>) -> Vec<Action> {
|
||||
Call::Show(view) => app(AppAction::ShowView(view)),
|
||||
Call::Buffers => app(AppAction::OpenPicker(PickKind::Document)),
|
||||
Call::Buffer(name) => app(AppAction::SwitchDocument(DocumentChoice::Named(name))),
|
||||
Call::Style(style) => vec![Action::edit(EditOp::SetStyle { id, style: Some(style) })],
|
||||
Call::Fold(fold) => set_fold(ctx, fold),
|
||||
Call::Style(style) => each(&nodes, |id| EditOp::SetStyle { id, style: Some(style.clone()) }),
|
||||
Call::Fold(fold) if on.is_empty() => set_fold(ctx, fold),
|
||||
Call::Fold(fold) => {
|
||||
let ids = on.iter().copied().filter(|&id| !ctx.doc.children(id).is_empty()).collect();
|
||||
vec![Action::edit(EditOp::SetFold { ids, fold })]
|
||||
}
|
||||
Call::Focus => vec![Action::App(AppAction::ToggleFocus)],
|
||||
Call::Color(color) => vec![Action::edit(EditOp::SetColor { id, color })],
|
||||
Call::Border(border) => vec![Action::edit(EditOp::SetBorder { id, border: Some(border) })],
|
||||
Call::Color(color) => each(&nodes, |id| EditOp::SetColor { id, color: color.clone() }),
|
||||
Call::Border(border) => each(&nodes, |id| EditOp::SetBorder { id, border: Some(border) }),
|
||||
Call::Tags(changes) => {
|
||||
let (mut add, mut remove) = (Vec::new(), Vec::new());
|
||||
for change in changes {
|
||||
@@ -52,7 +61,7 @@ pub fn actions_for(call: Call, ctx: &ViewContext<'_>) -> Vec<Action> {
|
||||
TagChange::Remove(tag) => remove.push(tag),
|
||||
}
|
||||
}
|
||||
vec![Action::edit(EditOp::Tag { id, add, remove })]
|
||||
each(&nodes, |id| EditOp::Tag { id, add: add.clone(), remove: remove.clone() })
|
||||
}
|
||||
// A document's name is its root's title.
|
||||
Call::Rename(title) => vec![Action::edit(EditOp::SetTitle { id: ctx.doc.root(), title })],
|
||||
@@ -72,9 +81,9 @@ pub fn actions_for(call: Call, ctx: &ViewContext<'_>) -> Vec<Action> {
|
||||
Call::Registers => app(AppAction::OpenRegisters),
|
||||
Call::Theme(name) => vec![Action::App(AppAction::Set(SetArg::Value("theme".into(), name)))],
|
||||
Call::Config => app(AppAction::OpenConfig),
|
||||
Call::Tidy(_) | Call::Align(_) | Call::BranchOut | Call::Export { .. } => {
|
||||
later("this one comes in v1.1")
|
||||
}
|
||||
Call::Tidy(layout) => commands::tidy(ctx, &visual::tops(&nodes, ctx), layout),
|
||||
Call::Align(side) => commands::align(ctx, on, side),
|
||||
Call::BranchOut | Call::Export { .. } => later("this one comes in v1.1"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +91,15 @@ fn app(action: AppAction) -> Vec<Action> {
|
||||
vec![Action::App(action)]
|
||||
}
|
||||
|
||||
/// The same change on each node, as one undo step.
|
||||
fn each(nodes: &[NodeId], op: impl Fn(NodeId) -> EditOp) -> Vec<Action> {
|
||||
let group = Group::new();
|
||||
match nodes {
|
||||
[id] => vec![Action::edit(op(*id))],
|
||||
nodes => nodes.iter().map(|&id| Action::edit_in(op(id), group)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn say(message: Message) -> Vec<Action> {
|
||||
vec![Action::say(message)]
|
||||
}
|
||||
@@ -145,7 +163,7 @@ mod tests {
|
||||
|
||||
/// Runs a command line's actions on a buffer, as the app does.
|
||||
fn run(buffer: &mut Buffer, setup: &Setup, call: Call) {
|
||||
for action in actions_for(call, &setup.ctx(buffer.doc(), buffer.cursor())) {
|
||||
for action in actions_for(call, &setup.ctx(buffer.doc(), buffer.cursor()), &[]) {
|
||||
if let Action::Doc(action) = action {
|
||||
buffer.apply(action);
|
||||
}
|
||||
|
||||
+74
-4
@@ -5,9 +5,11 @@
|
||||
//! own meaning (in the Note view, `dd` will delete a line of text).
|
||||
//! [`shared`] answers the ones it leaves.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::action::{Action, AppAction, CursorAfter, DocAction, DocumentChoice, Message, PasteAt, SaveWhich};
|
||||
use super::action::{
|
||||
self, Action, AppAction, CursorAfter, DocAction, DocumentChoice, Message, PasteAt, SaveWhich,
|
||||
};
|
||||
use super::mode::Grab;
|
||||
use super::panes::Arrange;
|
||||
use super::popup::PickKind;
|
||||
@@ -15,9 +17,10 @@ use super::text_edit::{EditAt, StartEdit};
|
||||
use crate::input::command::{Command, Motion, Operator, Register, Target};
|
||||
use crate::input::engine::Typed;
|
||||
use crate::model::{
|
||||
CanvasPos, CanvasRect, CanvasSize, Clip, Direction, Document, EditOp, Fold, Group, Node, NodeId, Spot,
|
||||
Title, Visible,
|
||||
CanvasPos, CanvasRect, CanvasSize, Clip, Direction, Document, EditOp, Fold, Group, Layout, Node, NodeId,
|
||||
Side, Spot, Title, Visible,
|
||||
};
|
||||
use crate::view::canvas::arrange;
|
||||
use crate::view::canvas::placement::{below_spot, child_spot, first_child_spot, free_spot, spot_below};
|
||||
use crate::view::{ViewContext, ViewKind};
|
||||
|
||||
@@ -82,6 +85,8 @@ pub fn shared(typed: &Typed, ctx: &ViewContext<'_>) -> Option<Vec<Action>> {
|
||||
Command::AutoFold => set_fold(ctx, Fold::Auto),
|
||||
Command::OpenAllFolds => fold_all(ctx, Fold::Open),
|
||||
Command::CloseAllFolds => fold_all(ctx, Fold::Closed),
|
||||
Command::Tidy(layout) => tidy(ctx, &[cursor], layout),
|
||||
Command::Align(_) => say("select the nodes to line up first: v, then hjkl"),
|
||||
Command::MoveMode => app(AppAction::EnterMoveMode(Vec::new())),
|
||||
Command::ConnectMode => app(AppAction::EnterConnectMode(Vec::new())),
|
||||
Command::Visual => app(AppAction::Visual { branches: false }),
|
||||
@@ -440,6 +445,71 @@ pub fn paste(ctx: &ViewContext<'_>, clip: Clip, at: PasteAt, times: u32) -> Opti
|
||||
Some(EditOp::Paste { clip, spot, offsets })
|
||||
}
|
||||
|
||||
/// `<Space>=` and `:tidy`: the branches under `tops` laid out as `layout`
|
||||
/// (see [`arrange::tidy`]), as one undo step.
|
||||
pub fn tidy(ctx: &ViewContext<'_>, tops: &[NodeId], layout: Layout) -> Vec<Action> {
|
||||
let tops: Vec<NodeId> = tops.iter().copied().filter(|&top| !ctx.doc.children(top).is_empty()).collect();
|
||||
let what = match tops.as_slice() {
|
||||
[] => return say("nothing to tidy: no children under it"),
|
||||
[top] => ctx.doc.node(*top).map(|node| node.title().to_string()).unwrap_or_default(),
|
||||
tops => format!("{} branches", tops.len()),
|
||||
};
|
||||
let rects = ctx.box_rects();
|
||||
let places: Vec<(NodeId, CanvasPos)> =
|
||||
tops.iter().flat_map(|&top| arrange::tidy(ctx.doc, top, layout, &rects)).collect();
|
||||
let how = match layout {
|
||||
Layout::Map => "a mind map",
|
||||
Layout::Chart => "an org chart",
|
||||
Layout::Grid => "a grid",
|
||||
};
|
||||
if places.is_empty() {
|
||||
return say(&format!("{what} is tidy already, as {how}"));
|
||||
}
|
||||
// What's outside the branches stays where it is, even under them: say so.
|
||||
let moved: BTreeMap<NodeId, CanvasPos> = places.iter().copied().collect();
|
||||
let inside: BTreeSet<NodeId> = tops.iter().flat_map(|&top| ctx.doc.branch(top)).collect();
|
||||
let after: Vec<CanvasRect> = inside
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
rects.get(id).map(|rect| CanvasRect::new(moved.get(id).copied().unwrap_or(rect.pos), rect.size))
|
||||
})
|
||||
.collect();
|
||||
let covered = Visible::new(ctx.doc, ctx.cursor, ctx.settings.focus)
|
||||
.order
|
||||
.into_iter()
|
||||
.filter(|id| !inside.contains(id))
|
||||
.filter(|id| rects.get(id).is_some_and(|other| after.iter().any(|rect| rect.overlaps(*other))))
|
||||
.count();
|
||||
let text = match covered {
|
||||
0 => format!("tidied {what} as {how} · u undoes it"),
|
||||
count => format!("tidied {what} as {how} · it covers {} now · u undoes it", action::nodes(count)),
|
||||
};
|
||||
vec![Action::edit(EditOp::Reposition(places)), Action::say(Message::done(text))]
|
||||
}
|
||||
|
||||
/// `<Space>a` and `:align`: the selected nodes lined up on one side (see
|
||||
/// [`arrange::align`]), as one undo step.
|
||||
pub fn align(ctx: &ViewContext<'_>, nodes: &[NodeId], side: Side) -> Vec<Action> {
|
||||
if nodes.len() < 2 {
|
||||
return say("select the nodes to line up first: v, then hjkl");
|
||||
}
|
||||
let places = arrange::align(ctx.doc, nodes, side, &ctx.box_rects());
|
||||
let how = match side {
|
||||
Side::Left => "on the left",
|
||||
Side::Right => "on the right",
|
||||
Side::Top => "along the top",
|
||||
Side::Bottom => "along the bottom",
|
||||
Side::Center => "on a middle line",
|
||||
};
|
||||
if places.is_empty() {
|
||||
return say(&format!("they're lined up {how} already"));
|
||||
}
|
||||
vec![
|
||||
Action::edit(EditOp::Reposition(places)),
|
||||
Action::say(Message::done(format!("lined up {} {how}", action::nodes(nodes.len())))),
|
||||
]
|
||||
}
|
||||
|
||||
/// Where the cursor goes after a delete: to `next`, or the root when there's
|
||||
/// nowhere else.
|
||||
fn after(next: Option<NodeId>) -> CursorAfter {
|
||||
|
||||
+10
-2
@@ -14,6 +14,7 @@ use crate::input::command::{Bound, Command, help_for};
|
||||
use crate::input::engine::{Outcome, Typed};
|
||||
use crate::input::key::Key;
|
||||
use crate::input::mouse::{Cell, Gesture};
|
||||
use crate::model::NodeId;
|
||||
use crate::ui::command_line::LineOutcome;
|
||||
use crate::ui::document_bar::DocumentBar;
|
||||
|
||||
@@ -122,21 +123,28 @@ impl App {
|
||||
LineOutcome::Typing => {}
|
||||
LineOutcome::Close => self.shared.mode = Mode::Normal,
|
||||
LineOutcome::Run(text) => {
|
||||
let on = std::mem::take(&mut line.selection);
|
||||
self.shared.mode = Mode::Normal;
|
||||
if text.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
self.command_history.add(&text);
|
||||
self.run_line(&text);
|
||||
self.run_line_on(&text, &on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a command line (without its `:`).
|
||||
pub(super) fn run_line(&mut self, text: &str) {
|
||||
self.run_line_on(text, &[]);
|
||||
}
|
||||
|
||||
/// Runs a command line typed in visual mode, on the nodes that were
|
||||
/// selected (none: the cursor's).
|
||||
fn run_line_on(&mut self, text: &str, on: &[NodeId]) {
|
||||
let sources = self.sources();
|
||||
let actions = match parse(text, &sources) {
|
||||
Ok(call) => calls::actions_for(call, &self.shared.ctx(self.buffer())),
|
||||
Ok(call) => calls::actions_for(call, &self.shared.ctx(self.buffer()), on),
|
||||
Err(error) => vec![Action::say(Message::problem(error.to_string()))],
|
||||
};
|
||||
self.update(actions);
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ fn hints(mode: &Mode, view: ViewKind) -> &'static str {
|
||||
text_hints(edit)
|
||||
}
|
||||
(Mode::Visual(_), ViewKind::Canvas | ViewKind::Board) => {
|
||||
"hjkl more · o other end · d delete · y yank · > < in/out · M move · <Space>c color · Esc stop"
|
||||
"hjkl more · o other end · d delete · y yank · > < in/out · M move · <Space> color, align… · Esc stop"
|
||||
}
|
||||
(Mode::Visual(_), ViewKind::Note) => {
|
||||
"h/l the siblings too · o other end · d delete · y yank · > < in/out · <Space>c color · Esc stop"
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
//! works, checked end to end. The files have their own tests in
|
||||
//! `tests/files.rs`, and the config folder in `tests/config.rs`.
|
||||
|
||||
mod arrange;
|
||||
mod config;
|
||||
mod connect;
|
||||
mod files;
|
||||
@@ -505,7 +506,7 @@ fn indenting_and_outdenting_nodes() {
|
||||
#[test]
|
||||
fn v1_1_commands_say_so_and_portal_titles_stay() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys(":tidy<CR>");
|
||||
h.type_keys(":branchout<CR>");
|
||||
assert_eq!(h.message(), "this one comes in v1.1");
|
||||
// Kitchen is a portal: its title is the other document's name.
|
||||
h.type_keys("glgjgjgj");
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Tidying and lining up (part 9a), end to end: `<Space>=` and `:tidy` lay
|
||||
//! a branch out as a mind map, an org chart or a grid, and `<Space>a` and
|
||||
//! `:align` line up a selection.
|
||||
|
||||
use super::*;
|
||||
use crate::model::CanvasRect;
|
||||
|
||||
/// Where a node's box is now, on the canvas.
|
||||
fn rect(h: &Harness, title: &str) -> CanvasRect {
|
||||
h.app.shared.ctx(h.app.buffer()).box_rects()[&h.node(title)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tidy_lays_a_branch_out_as_a_mind_map() {
|
||||
let mut h = Harness::new();
|
||||
let root = h.pos("Garden plans");
|
||||
h.type_keys("<Space>=");
|
||||
assert!(h.screen().contains("tidy: a mind map"), "which-key lists them:\n{}", h.screen());
|
||||
h.type_keys("m");
|
||||
assert_eq!(h.message(), "tidied Garden plans as a mind map · it covers 1 node now · u undoes it");
|
||||
assert_eq!(h.pos("Garden plans"), root, "the top stays where it is");
|
||||
let (soil, kitchen) = (rect(&h, "Soil"), rect(&h, "Kitchen"));
|
||||
assert_eq!(soil.left(), kitchen.left(), "the children in a column");
|
||||
let middle = (soil.center().y + kitchen.center().y) / 2;
|
||||
assert_eq!(rect(&h, "Garden plans").center().y, middle, "the root in the middle of them");
|
||||
// Folded away or not, every node gets its place.
|
||||
assert_eq!(rect(&h, "Cedar, untreated").left(), rect(&h, "Raised beds").right() + 6);
|
||||
h.type_keys("u");
|
||||
assert_eq!(h.pos("Soil"), CanvasPos::new(22, 2), "one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chart_sends_its_lines_down_and_a_grid_makes_a_block() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys(":tidy chart<CR>");
|
||||
let (root, soil) = (rect(&h, "Garden plans"), rect(&h, "Soil"));
|
||||
assert_eq!(soil.top(), root.bottom() + 2, "the children below");
|
||||
let (_, row) = h.find("┃ Garden plans ┃");
|
||||
let below = &h.rows()[usize::from(row) + 1];
|
||||
assert!(below.contains('┯') || below.contains('┬'), "the lines leave downward:\n{}", h.screen());
|
||||
// A new child goes beside the last one.
|
||||
h.type_keys("aCompost<Esc><Esc>");
|
||||
let (compost, kitchen) = (rect(&h, "Compost"), rect(&h, "Kitchen"));
|
||||
assert_eq!((compost.top(), compost.left() > kitchen.right()), (kitchen.top(), true));
|
||||
// The Inbox's ideas, as a block of cards beside it.
|
||||
h.type_keys(":in mulch<CR>:in seeds<CR>:in hoe<CR>:in worms<CR>:find inbox<CR><Space>=g");
|
||||
assert_eq!(h.message(), "tidied Inbox as a grid · u undoes it");
|
||||
let (mulch, seeds, hoe) = (rect(&h, "mulch"), rect(&h, "seeds"), rect(&h, "hoe"));
|
||||
assert_eq!((mulch.top(), mulch.left()), (rect(&h, "Inbox").top(), hoe.left()), "two by two");
|
||||
assert_eq!(mulch.top(), seeds.top());
|
||||
// Only the first column is joined: a line into seeds would show between
|
||||
// the two cards.
|
||||
let (col, row) = h.find("seeds");
|
||||
assert_eq!(h.rows()[usize::from(row)].chars().nth(usize::from(col) - 2), Some('│'), "{}", h.screen());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn align_lines_up_a_selection() {
|
||||
let mut h = Harness::new();
|
||||
let barrel = h.pos("Rain barrel");
|
||||
// Soil, Raised beds and Watering, their right edges in a line.
|
||||
h.type_keys("ggglvjj<Space>a");
|
||||
assert!(h.screen().contains("middles in a column"), "which-key lists the sides:\n{}", h.screen());
|
||||
h.type_keys("l");
|
||||
assert_eq!((h.label(), h.message()), ("NORMAL", "lined up 3 nodes on the right"));
|
||||
let right = rect(&h, "Raised beds").right();
|
||||
assert_eq!((rect(&h, "Soil").right(), rect(&h, "Watering").right()), (right, right));
|
||||
let moved = h.pos("Watering").x - 22;
|
||||
assert_eq!(h.pos("Rain barrel"), CanvasPos::new(barrel.x + moved, barrel.y), "its branch came along");
|
||||
// : in visual mode runs a command on the selection.
|
||||
h.type_keys("uggglvjj:");
|
||||
assert!(h.rows()[h.rows().len() - 2].contains("3 nodes selected"), "what it runs on:\n{}", h.screen());
|
||||
h.type_keys("align center<CR>");
|
||||
assert_eq!(h.message(), "lined up 3 nodes on a middle line");
|
||||
let middle = rect(&h, "Soil").center().x;
|
||||
assert_eq!((rect(&h, "Raised beds").center().x, rect(&h, "Watering").center().x), (middle, middle));
|
||||
h.type_keys("ggglvjj:color red<CR>");
|
||||
let red = |h: &Harness, title: &str| {
|
||||
h.doc().node(h.node(title)).unwrap().look.color.as_ref().is_some_and(|color| color.as_str() == "red")
|
||||
};
|
||||
assert!(["Soil", "Raised beds", "Watering"].iter().all(|title| red(&h, title)));
|
||||
h.type_keys("u");
|
||||
assert!(!red(&h, "Soil") && !red(&h, "Watering"), "one undo step");
|
||||
// Without a selection, :align says what it needs.
|
||||
h.type_keys(":align left<CR>");
|
||||
assert_eq!(h.message(), "select the nodes to line up first: v, then hjkl");
|
||||
}
|
||||
+3
-1
@@ -132,9 +132,11 @@ impl App {
|
||||
}
|
||||
AppAction::LeaveMode => self.shared.mode = Mode::Normal,
|
||||
AppAction::OpenCommandLine => {
|
||||
// From visual mode, the line acts on the selection.
|
||||
let selection = self.selection().unwrap_or_default();
|
||||
let line =
|
||||
CommandLine::new(&self.sources(), &self.command_history, self.settings.completion_popup);
|
||||
self.shared.mode = Mode::Command(line);
|
||||
self.shared.mode = Mode::Command(line.on(selection));
|
||||
}
|
||||
AppAction::CommandLineWith(text) => {
|
||||
let (sources, popup) = (self.sources(), self.settings.completion_popup);
|
||||
|
||||
+20
-2
@@ -69,7 +69,12 @@ pub fn selection(visual: &Visual, ctx: &ViewContext<'_>, kind: ViewKind) -> Vec<
|
||||
impl App {
|
||||
/// The selected nodes, in visual mode, as the focused view takes them.
|
||||
pub(super) fn selection(&self) -> Option<Vec<NodeId>> {
|
||||
let Mode::Visual(visual) = &self.shared.mode else { return None };
|
||||
let visual = match &self.shared.mode {
|
||||
Mode::Visual(visual) => visual,
|
||||
// A command line typed in visual mode keeps what was selected.
|
||||
Mode::Command(line) if !line.selection.is_empty() => return Some(line.selection.clone()),
|
||||
_ => return None,
|
||||
};
|
||||
let pane = self.panes.current();
|
||||
Some(selection(visual, &self.shared.ctx(pane.buffer), pane.view.kind()))
|
||||
}
|
||||
@@ -172,6 +177,19 @@ pub fn command(
|
||||
Command::MoveMode => {
|
||||
say("several nodes move on the canvas; here, > and < move them through the tree")
|
||||
}
|
||||
// Each selected branch, from its top, laid out; or the nodes lined up
|
||||
// (with `V`, each branch by its top, the rest coming along).
|
||||
Command::Tidy(layout) => {
|
||||
let mut actions = vec![leave];
|
||||
actions.extend(commands::tidy(ctx, &tops(selected, ctx), layout));
|
||||
actions
|
||||
}
|
||||
Command::Align(side) => {
|
||||
let nodes = if visual.branches { tops(selected, ctx) } else { selected.to_vec() };
|
||||
let mut actions = vec![leave];
|
||||
actions.extend(commands::align(ctx, &nodes, side));
|
||||
actions
|
||||
}
|
||||
// Every node connects to the one the cursor goes to; with `V`, each
|
||||
// branch from its top.
|
||||
Command::ConnectMode => {
|
||||
@@ -215,7 +233,7 @@ fn delete(
|
||||
}
|
||||
|
||||
/// The selected nodes that aren't under another selected node.
|
||||
fn tops(selected: &[NodeId], ctx: &ViewContext<'_>) -> Vec<NodeId> {
|
||||
pub(super) fn tops(selected: &[NodeId], ctx: &ViewContext<'_>) -> Vec<NodeId> {
|
||||
let set: BTreeSet<NodeId> = selected.iter().copied().collect();
|
||||
selected
|
||||
.iter()
|
||||
|
||||
Reference in new issue
Block a user