Part 8d, the last of stage 8, and with it everything on DESIGN.md's v1 list. Connect mode (C): the cursor goes to another node, a dashed arrow in the visual color follows it, and Enter links the two; then the cursor goes back to where C was typed (the user's pick). d removes the line between them (a branch is cut, and the child comes loose), > puts the node under the other one, and <Space> a, l, c, s and t style the line as you watch, making it when it isn't there yet. Everything connect mode changes is one undo group, and Esc takes it back with History::cancel, which leaves nothing to redo. From visual mode every selected node connects, and the line pickers go back to connect mode when they close. Portals show their document live, from a cache of what each document file holds (open documents from their buffers, the others from their files, looked at every second): the root's title, its color and how many nodes it has, or ? when the document isn't there. Enter or gf goes through, as a jump, and the pane's breadcrumb starts with the documents it came through. <Space>p types :portal (the user's pick): a path as :e takes it, or the title of a new document. Backlinks are put right whenever a document is opened or saved; the root shows how many, the Note view lists them, gu goes up to them (the user's pick) and :backlinks lists them. File names follow the root's title: when it changed, the next save renames the file (-2 when the name is taken; a folder's default document keeps its name), and the portals pointing at it, its backlinks in the documents it leads to, the recent documents, the capital marks and the jump lists follow. A portal whose file was moved by hand finds its document in that folder by uid. Portals in a register keep full paths, so a pasted portal leads to the same document from any folder. Co-Authored-By: Claude Opus 5.5 <[email protected]>
359 lines
11 KiB
Rust
359 lines
11 KiB
Rust
//! Actions: what views, keys and the command line ask for. Views never
|
|
//! change the document, the cursor or the mode themselves. They answer with
|
|
//! actions, and the app carries them out: [`Buffer::apply`] for a document
|
|
//! and its cursor, the app for the rest.
|
|
//!
|
|
//! [`Buffer::apply`]: super::buffer::Buffer::apply
|
|
|
|
use super::documents::BufferId;
|
|
use super::jumps::Place;
|
|
use super::mode::Connect;
|
|
use super::panes::Arrange;
|
|
use super::popup::{MenuFor, PickKind};
|
|
use super::repeat::Look;
|
|
use super::text_edit::StartEdit;
|
|
use crate::command::parse::SetArg;
|
|
use crate::config::ConfigFile;
|
|
use crate::input::command::{Command, Register};
|
|
use crate::input::key::Key;
|
|
use crate::model::{Direction, EditOp, Group, NodeId, Title};
|
|
use crate::view::ViewKind;
|
|
|
|
/// How a status-bar message is meant.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Tone {
|
|
/// Something to know.
|
|
Info,
|
|
/// Something got done.
|
|
Done,
|
|
/// Something didn't work.
|
|
Problem,
|
|
}
|
|
|
|
/// A message for the status bar.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Message {
|
|
pub text: String,
|
|
pub tone: Tone,
|
|
}
|
|
|
|
/// "1 node", "3 nodes".
|
|
pub fn nodes(count: usize) -> String {
|
|
if count == 1 { "1 node".to_string() } else { format!("{count} nodes") }
|
|
}
|
|
|
|
impl Message {
|
|
pub fn info(text: impl Into<String>) -> Message {
|
|
Message { text: text.into(), tone: Tone::Info }
|
|
}
|
|
|
|
pub fn done(text: impl Into<String>) -> Message {
|
|
Message { text: text.into(), tone: Tone::Done }
|
|
}
|
|
|
|
pub fn problem(text: impl Into<String>) -> Message {
|
|
Message { text: text.into(), tone: Tone::Problem }
|
|
}
|
|
|
|
/// Several messages in one, with ` · ` between them: a problem if one of
|
|
/// them is. `None` when there are none.
|
|
pub fn joined(messages: Vec<Message>) -> Option<Message> {
|
|
let problem = messages.iter().any(|message| message.tone == Tone::Problem);
|
|
let tone = if problem { Tone::Problem } else { messages.first()?.tone };
|
|
let texts: Vec<&str> = messages.iter().map(|message| message.text.as_str()).collect();
|
|
Some(Message { text: texts.join(" · "), tone })
|
|
}
|
|
}
|
|
|
|
/// Where the cursor goes after an edit.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum CursorAfter {
|
|
/// Where it is (or to the root, if its node is gone).
|
|
Stay,
|
|
/// To the node the edit added.
|
|
Added,
|
|
/// To this node.
|
|
To(NodeId),
|
|
}
|
|
|
|
/// What happens to a document and its cursor.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum DocAction {
|
|
/// Move the cursor. A `jump` is one that `''` can come back from.
|
|
MoveCursor {
|
|
to: NodeId,
|
|
jump: bool,
|
|
},
|
|
/// Change the document, as one undo step (or as part of its group's).
|
|
/// The op is boxed because it can hold whole nodes, which would make
|
|
/// every action that size.
|
|
Edit {
|
|
op: Box<EditOp>,
|
|
cursor: CursorAfter,
|
|
group: Option<Group>,
|
|
},
|
|
Undo(u32),
|
|
Redo(u32),
|
|
SetMark(char),
|
|
/// Go to a mark; `'` and `` ` `` go back to before the last jump.
|
|
JumpToMark(char),
|
|
/// Take back the group's undo step, if it's the newest, leaving nothing
|
|
/// to redo: Esc in connect mode.
|
|
Cancel(Group),
|
|
}
|
|
|
|
/// What happens around the documents.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum AppAction {
|
|
/// Start editing the cursor's title, tags or body.
|
|
StartEdit(StartEdit),
|
|
/// The text edit is done: back to normal mode, or to the `K` card the
|
|
/// body was edited in.
|
|
EndEdit,
|
|
/// Put the text cursor here, in the text being edited (a click).
|
|
TextCursor(usize),
|
|
/// Visual mode, `V` (`branches`) or `v`; in visual mode, switches
|
|
/// between the two.
|
|
Visual {
|
|
branches: bool,
|
|
},
|
|
/// In visual mode: the cursor goes to the other end (`o`).
|
|
SwapEnds,
|
|
/// A node into the selection, or out of it (a Ctrl+click).
|
|
ToggleSelected(NodeId),
|
|
/// Nodes into the selection (a Ctrl+drag).
|
|
SelectAlso(Vec<NodeId>),
|
|
/// Move mode: hjkl moves the cursor's branch, or these nodes (a
|
|
/// selection's).
|
|
EnterMoveMode(Vec<NodeId>),
|
|
/// In move mode: the whole branch, or just the node.
|
|
ToggleWhole,
|
|
/// Connect mode (`C`): from the cursor's node, or from these (a
|
|
/// selection's).
|
|
EnterConnectMode(Vec<NodeId>),
|
|
/// Back to connect mode, from a picker it opened.
|
|
ResumeConnect(Connect),
|
|
/// Enter or `gf` on a portal: through it, to its document's root.
|
|
FollowPortal(NodeId),
|
|
/// `:portal`: a portal under the cursor's node, to the document at the
|
|
/// path typed (a new document when nothing's there).
|
|
NewPortal(String),
|
|
/// `gu`: up to the portals that lead to this document; `list`
|
|
/// (`:backlinks`) lists them even when there's just one.
|
|
GoUp {
|
|
list: bool,
|
|
},
|
|
/// Back to normal mode.
|
|
LeaveMode,
|
|
OpenCommandLine,
|
|
/// The command line, with this typed already: a `:` command from the
|
|
/// help that needs more.
|
|
CommandLineWith(String),
|
|
/// A command line, run as if typed (without its `:`).
|
|
RunLine(String),
|
|
/// A command, run as if its keys were typed: the help and the menu run
|
|
/// commands this way.
|
|
Run(Command),
|
|
/// Keys, as if typed: for a key that waits for more (`m` waits for a
|
|
/// mark's letter).
|
|
Type(Vec<Key>),
|
|
/// Keep nodes in a register (`yy`, and before a delete): exactly
|
|
/// `nodes`, or (`branches`) each with its whole branch.
|
|
Yank {
|
|
register: Option<Register>,
|
|
nodes: Vec<NodeId>,
|
|
branches: bool,
|
|
},
|
|
/// Paste what a register holds (`p`, `P`): its nodes, or its text
|
|
/// made into nodes, `times` copies.
|
|
Paste {
|
|
register: Option<Register>,
|
|
at: PasteAt,
|
|
times: u32,
|
|
},
|
|
/// A value was picked in a picker: `.` can put it on another node.
|
|
Picked(Look),
|
|
/// Focus mode: Open branches act like Auto ones.
|
|
ToggleFocus,
|
|
ToggleWhichKey,
|
|
/// Change a setting.
|
|
Set(SetArg),
|
|
|
|
// Popups.
|
|
/// Every key and command, with this already searched for.
|
|
OpenHelp(String),
|
|
/// `/`: the search.
|
|
OpenSearch,
|
|
/// A search went somewhere: `n` and `N` go on with it.
|
|
Searched(String),
|
|
/// `Ctrl-o` (`back`) and `Ctrl-i`: through the focused pane's jump list.
|
|
Travel {
|
|
back: bool,
|
|
times: u32,
|
|
},
|
|
/// `n` and `N`: the next or previous match of the last search.
|
|
FindAgain {
|
|
forward: bool,
|
|
times: u32,
|
|
},
|
|
/// `:find`: straight to the best match.
|
|
Find(String),
|
|
/// A capital mark on the cursor's node (`mA`).
|
|
SetFileMark(char),
|
|
/// To a capital mark, in any document (`'A`).
|
|
JumpToFileMark(char),
|
|
/// To a place from the jump list (`:jumps`).
|
|
GoToPlace(Place),
|
|
/// `:marks`, `:jumps` and `:registers`.
|
|
OpenMarks,
|
|
OpenJumps,
|
|
OpenRegisters,
|
|
/// `gd` on a node with several cross-links: which one to follow.
|
|
OpenLinks,
|
|
/// The cursor's body in a card beside it.
|
|
OpenCard,
|
|
OpenPicker(PickKind),
|
|
/// The right-click menu, at a cell on the screen.
|
|
OpenMenu {
|
|
what: MenuFor,
|
|
col: u16,
|
|
row: u16,
|
|
},
|
|
|
|
// Panes and documents.
|
|
/// Split the focused pane: the new pane shows `view`, or the same view.
|
|
Split {
|
|
arrange: Arrange,
|
|
view: Option<ViewKind>,
|
|
},
|
|
FocusPane(Direction),
|
|
NextPane,
|
|
/// Close the focused pane (`:q`, `ZZ`, `Ctrl-w c`). The last one quits,
|
|
/// unless a document has unsaved changes; `force` (`:q!`) quits anyway.
|
|
ClosePane {
|
|
force: bool,
|
|
},
|
|
OnlyPane,
|
|
EqualizePanes,
|
|
/// The focused pane shows another view.
|
|
ShowView(ViewKind),
|
|
SwitchDocument(DocumentChoice),
|
|
|
|
// Files.
|
|
/// `:config`: the config files, to open one in the editor.
|
|
OpenConfig,
|
|
/// Open a config file in the editor (made first if it isn't there).
|
|
EditConfig(ConfigFile),
|
|
/// Save (`:w`, `:x`, `:wa`).
|
|
Save(SaveWhich),
|
|
/// Open a document in the focused pane (`:e`), from a path as typed:
|
|
/// relative to the focused document's folder, `~` for the home folder.
|
|
/// No path reads the focused document again from its file; `force`
|
|
/// (`:e!`) drops unsaved changes to do it.
|
|
Edit {
|
|
path: Option<String>,
|
|
force: bool,
|
|
},
|
|
/// Make a new document next to the focused one, and show it (`:new`).
|
|
NewDocument(Option<Title>),
|
|
/// Close the focused document (`:bd`); `force` drops unsaved changes.
|
|
CloseDocument {
|
|
force: bool,
|
|
},
|
|
/// A file changed outside cumulus while its document had unsaved
|
|
/// changes here: which version to keep.
|
|
Resolve {
|
|
buffer: BufferId,
|
|
keep: Keep,
|
|
},
|
|
|
|
/// Quit cumulus (`:qa`): not while a document has unsaved changes,
|
|
/// unless `force` (`:qa!`).
|
|
Quit {
|
|
force: bool,
|
|
},
|
|
Say(Message),
|
|
}
|
|
|
|
/// 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 last children (`p`).
|
|
Child,
|
|
/// Its next siblings, below it, where `o` adds one (`P`).
|
|
Sibling,
|
|
}
|
|
|
|
/// What `:w` and its kin save.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SaveWhich {
|
|
/// The focused document, changed or not (`:w`).
|
|
This,
|
|
/// The focused document, if it has unsaved changes (`:x`, `ZZ`).
|
|
IfChanged,
|
|
/// Every document with unsaved changes (`:wa`).
|
|
All,
|
|
}
|
|
|
|
/// Which version of a document to keep when its file changed outside
|
|
/// cumulus while it had unsaved changes here.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Keep {
|
|
/// The one on screen: it's saved over the file.
|
|
Mine,
|
|
/// The file's: it takes the place of the one on screen (`u` brings that
|
|
/// back).
|
|
Theirs,
|
|
}
|
|
|
|
/// Which document the focused pane switches to.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum DocumentChoice {
|
|
Next,
|
|
Previous,
|
|
/// The open document listed at this place (the documents picker).
|
|
At(usize),
|
|
/// The open document with this name (`:b`).
|
|
Named(String),
|
|
}
|
|
|
|
/// Anything views, keys and the command line ask for.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Action {
|
|
Doc(DocAction),
|
|
App(AppAction),
|
|
}
|
|
|
|
impl Action {
|
|
/// Move the cursor to a node.
|
|
pub fn cursor(to: NodeId) -> Action {
|
|
Action::Doc(DocAction::MoveCursor { to, jump: false })
|
|
}
|
|
|
|
/// Jump to a node: `''` comes back.
|
|
pub fn jump(to: NodeId) -> Action {
|
|
Action::Doc(DocAction::MoveCursor { to, jump: true })
|
|
}
|
|
|
|
/// Change the document, as one undo step; the cursor stays.
|
|
pub fn edit(op: EditOp) -> Action {
|
|
Action::Doc(DocAction::Edit { op: Box::new(op), cursor: CursorAfter::Stay, group: None })
|
|
}
|
|
|
|
/// Change the document, as one undo step, then move the cursor.
|
|
pub fn edit_then(op: EditOp, cursor: CursorAfter) -> Action {
|
|
Action::Doc(DocAction::Edit { op: Box::new(op), cursor, group: None })
|
|
}
|
|
|
|
/// Change the document as part of a group's undo step.
|
|
pub fn edit_in(op: EditOp, group: Group) -> Action {
|
|
Action::Doc(DocAction::Edit { op: Box::new(op), cursor: CursorAfter::Stay, group: Some(group) })
|
|
}
|
|
|
|
/// Show a message.
|
|
pub fn say(message: Message) -> Action {
|
|
Action::App(AppAction::Say(message))
|
|
}
|
|
}
|