Add connect mode, portals, backlinks and renames (stage 8d, v1)

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]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-02 08:24:43 -04:00
1 parent bd819409f4
commit ebe8a5988e
45 files changed
+2329 -148

No files matched your search

+52 -12
View File
@@ -24,14 +24,16 @@ pub mod calls;
mod clipboard;
pub mod commands;
mod configure;
pub mod connect;
pub mod documents;
mod files;
mod input;
mod jumps;
pub mod jumps;
mod marks;
pub mod mode;
pub mod panes;
pub mod popup;
pub mod portals;
pub mod registers;
mod render;
mod repeat;
@@ -43,7 +45,7 @@ mod update;
pub mod visual;
mod yank;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::time::Instant;
use ratatui::layout::Rect;
@@ -59,6 +61,7 @@ use crate::input::bindings::Context;
use crate::input::engine::KeyEngine;
use crate::input::keymap::Keymap;
use crate::input::mouse::MouseReader;
use crate::model::DocUid;
use crate::storage::memory::Memory;
use crate::ui::theme::Theme;
use crate::view::canvas::{Camera, CanvasView};
@@ -71,6 +74,7 @@ use documents::{BufferId, Documents};
use jumps::Jumps;
use mode::Mode;
use panes::{PaneId, Panes};
use portals::Portals;
use registers::Registers;
use startup::Opening;
@@ -121,18 +125,28 @@ struct Shared {
mode: Mode,
theme: Theme,
view: ViewSettings,
/// What the documents portals lead to look like.
portals: Portals,
}
impl Shared {
/// What a view showing document `buffer` looks at.
fn ctx(&self, buffer: BufferId) -> ViewContext<'_> {
let buffer = &self.docs.get(buffer).buffer;
self.ctx_in(buffer, &self.mode)
}
/// What a view showing document `buffer` looks at, in `mode` (the panes
/// without the focus are drawn as in normal mode).
fn ctx_in<'a>(&'a self, buffer: BufferId, mode: &'a Mode) -> ViewContext<'a> {
let open = self.docs.get(buffer);
ViewContext {
doc: buffer.doc(),
cursor: buffer.cursor(),
mode: &self.mode,
doc: open.buffer.doc(),
cursor: open.buffer.cursor(),
mode,
theme: &self.theme,
settings: &self.view,
portals: &self.portals,
folder: open.file.path.parent().unwrap_or(Path::new(".")),
}
}
@@ -142,11 +156,23 @@ impl Shared {
}
}
/// A pane: one of the documents, through a view, and where it jumped from.
/// A pane: one of the documents, through a view, where it jumped from, and
/// the portals it came through.
struct Pane {
buffer: BufferId,
view: Box<dyn View>,
jumps: Jumps,
/// The documents the pane came to this one through, by portals, the
/// first one first: the breadcrumb starts with them.
trail: Vec<Crumb>,
}
/// A document a pane came through, by a portal.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Crumb {
pub uid: DocUid,
/// Its name then.
pub title: String,
}
impl Pane {
@@ -160,14 +186,24 @@ impl Pane {
if let Some(camera) = camera {
view.set_camera(camera);
}
Pane { buffer, view, jumps: Jumps::default() }
Pane { buffer, view, jumps: Jumps::default(), trail: Vec::new() }
}
/// The pane shows `buffer` in a new view (a canvas starting at `camera`);
/// its jump list stays.
/// its jump list and its trail stay.
fn show(&mut self, buffer: BufferId, kind: ViewKind, camera: Option<Camera>) {
let jumps = std::mem::take(&mut self.jumps);
*self = Pane { jumps, ..Pane::new(buffer, kind, camera) };
let (jumps, trail) = (std::mem::take(&mut self.jumps), std::mem::take(&mut self.trail));
*self = Pane { jumps, trail, ..Pane::new(buffer, kind, camera) };
}
/// The pane got to document `uid` another way than a portal: if it came
/// through that one, the trail goes back to it; otherwise it starts
/// again.
fn arrive(&mut self, uid: DocUid) {
match self.trail.iter().position(|crumb| crumb.uid == uid) {
Some(at) => self.trail.truncate(at),
None => self.trail.clear(),
}
}
}
@@ -193,7 +229,7 @@ impl App {
let first = docs.at(0);
let mut app = App {
panes: Panes::new(Pane::new(first, ViewKind::Canvas, None)),
shared: Shared { docs, mode: Mode::Normal, theme, view },
shared: Shared { docs, mode: Mode::Normal, theme, view, portals: Portals::default() },
command_history: CommandHistory::from_lines(memory.history.clone()),
paths,
settings,
@@ -223,6 +259,10 @@ impl App {
for loaded in others {
said.extend(app.add_document(loaded).1);
}
// Once every document named is open, so one that's about to open
// isn't written underneath it.
app.sync_backlinks(first);
app.refresh_portals(true);
app.message = Message::joined(said);
app
}