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:
1 parent
bd819409f4
commit
ebe8a5988e
45 files changed
+2329
-148
No files matched your search
@@ -19,6 +19,9 @@ pub enum LineOwner {
|
||||
Branch(NodeId),
|
||||
/// A cross-link.
|
||||
Link(LinkKey),
|
||||
/// The line connect mode shows from a node to the cursor's, before
|
||||
/// Enter makes it.
|
||||
Preview,
|
||||
}
|
||||
|
||||
/// The color and owner given to the cells a line or border passes through.
|
||||
|
||||
@@ -13,10 +13,11 @@ use super::boxes::{NodeBox, draft_box, truncate};
|
||||
use super::camera::{Camera, ScreenPos, ScreenRect, Zoom};
|
||||
use super::lines::{LineLayer, LineOwner, Paint};
|
||||
use super::route::{Bend, route};
|
||||
use crate::app::connect;
|
||||
use crate::app::mode::Mode;
|
||||
use crate::app::text_edit::TextEdit;
|
||||
use crate::app::visual::selection;
|
||||
use crate::model::{Arrows, Border, Content, Document, Node, NodeId, Place, Visible};
|
||||
use crate::model::{Arrows, Border, Content, Document, LineRef, Node, NodeId, Place, Stroke, Visible};
|
||||
use crate::ui::glyphs::Glyphs;
|
||||
use crate::ui::text_editor::layout;
|
||||
use crate::ui::theme::{LineStyle, Theme};
|
||||
@@ -108,8 +109,10 @@ impl Scene {
|
||||
let visible = Visible::new(doc, cursor, settings.focus);
|
||||
let picked: BTreeSet<NodeId> = match ctx.mode {
|
||||
Mode::Visual(visual) => selection(visual, ctx, ViewKind::Canvas).into_iter().collect(),
|
||||
Mode::Connect(connecting) => connect::others(connecting, cursor).into_iter().collect(),
|
||||
_ => BTreeSet::new(),
|
||||
};
|
||||
let aim = Aim::new(ctx);
|
||||
let mut scene = Scene {
|
||||
size,
|
||||
zoom: camera.zoom,
|
||||
@@ -124,8 +127,11 @@ impl Scene {
|
||||
};
|
||||
for &id in &visible.order {
|
||||
let Some(node) = doc.node(id) else { continue };
|
||||
let style = theme.node_style(node, id == doc.root());
|
||||
let badge = visible.hidden.get(&id).map(|count| format!("+{count}")).unwrap_or_default();
|
||||
let style = ctx.node_style(id, node);
|
||||
let mut badge = ctx.badge(id, node).unwrap_or_default();
|
||||
if let Some(count) = visible.hidden.get(&id) {
|
||||
badge = if badge.is_empty() { format!("+{count}") } else { format!("{badge} +{count}") };
|
||||
}
|
||||
let max_width = settings.max_node_width;
|
||||
let (rect, shape) = match edit.filter(|edit| edit.node == id && camera.zoom == Zoom::Normal) {
|
||||
Some(edit) => scene.draft(node, edit, badge, style.border, camera, max_width),
|
||||
@@ -143,7 +149,7 @@ impl Scene {
|
||||
scene.index = scene.nodes.iter().enumerate().map(|(index, node)| (node.id, index)).collect();
|
||||
scene.cover();
|
||||
scene.add_borders();
|
||||
scene.add_lines(doc, theme);
|
||||
scene.add_lines(doc, theme, &aim);
|
||||
scene
|
||||
}
|
||||
|
||||
@@ -248,13 +254,18 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Adds every branch (from a shown parent to a shown child) and every
|
||||
/// cross-link between shown nodes.
|
||||
fn add_lines(&mut self, doc: &Document, theme: &Theme) {
|
||||
/// cross-link between shown nodes. In connect mode, the lines aimed at
|
||||
/// stand out in the visual color, and a dashed arrow runs to the cursor
|
||||
/// from each node with no line to it yet.
|
||||
fn add_lines(&mut self, doc: &Document, theme: &Theme, aim: &Aim) {
|
||||
for child_index in 0..self.nodes.len() {
|
||||
let child = &self.nodes[child_index];
|
||||
let Some(Place::Child { parent, branch }) = doc.place(child.id) else { continue };
|
||||
let Some(&parent_index) = self.index.get(parent) else { continue };
|
||||
let style = theme.branch_style(branch, self.nodes[parent_index].color);
|
||||
let mut style = theme.branch_style(branch, self.nodes[parent_index].color);
|
||||
if aim.lines.contains(&LineRef::Branch(child.id)) {
|
||||
style.color = self.visual;
|
||||
}
|
||||
let owner = LineOwner::Branch(child.id);
|
||||
self.connect(parent_index, child_index, Bend::NearStart, style, owner, None);
|
||||
}
|
||||
@@ -263,10 +274,20 @@ impl Scene {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let style = theme.link_style(&link.look, self.nodes[start].color);
|
||||
let mut style = theme.link_style(&link.look, self.nodes[start].color);
|
||||
if aim.lines.contains(&LineRef::Link(link.key())) {
|
||||
style.color = self.visual;
|
||||
}
|
||||
let label = theme.link_label(link);
|
||||
self.connect(start, end, Bend::Halfway, style, LineOwner::Link(link.key()), label);
|
||||
}
|
||||
for &(source, target) in &aim.previews {
|
||||
let (Some(&start), Some(&end)) = (self.index.get(&source), self.index.get(&target)) else {
|
||||
continue;
|
||||
};
|
||||
let style = LineStyle { stroke: Stroke::Dashed, arrows: Arrows::End, color: self.visual };
|
||||
self.connect(start, end, Bend::Halfway, style, LineOwner::Preview, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws one line from node `from` to node `to` (indexes in `nodes`),
|
||||
@@ -336,6 +357,30 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// What connect mode aims at: the lines already there between the nodes
|
||||
/// being connected and the cursor's, and the pairs with no line yet.
|
||||
#[derive(Debug, Default)]
|
||||
struct Aim {
|
||||
lines: BTreeSet<LineRef>,
|
||||
previews: Vec<(NodeId, NodeId)>,
|
||||
}
|
||||
|
||||
impl Aim {
|
||||
fn new(ctx: &ViewContext<'_>) -> Aim {
|
||||
let mut aim = Aim::default();
|
||||
let Mode::Connect(connecting) = ctx.mode else { return aim };
|
||||
for source in connect::others(connecting, ctx.cursor) {
|
||||
match ctx.doc.line_between(source, ctx.cursor) {
|
||||
Some(line) => {
|
||||
aim.lines.insert(line);
|
||||
}
|
||||
None => aim.previews.push((source, ctx.cursor)),
|
||||
}
|
||||
}
|
||||
aim
|
||||
}
|
||||
}
|
||||
|
||||
/// The index of the topmost node on a cell, in a `covered` grid.
|
||||
fn covered_at(covered: &[Option<usize>], size: Size, at: ScreenPos) -> Option<usize> {
|
||||
let (width, height) = (i32::from(size.width), i32::from(size.height));
|
||||
|
||||
@@ -75,10 +75,11 @@ impl CanvasView {
|
||||
Scene::build(ctx, self.camera, self.area.as_size())
|
||||
}
|
||||
|
||||
/// A left press: on a node, selects it and starts dragging its branch;
|
||||
/// on empty space, starts panning.
|
||||
fn press(&mut self, at: ScreenPos) -> Vec<Action> {
|
||||
/// A left press: on a node, selects it and starts dragging its branch
|
||||
/// (in connect mode, just aims at it); on empty space, starts panning.
|
||||
fn press(&mut self, at: ScreenPos, ctx: &ViewContext<'_>) -> Vec<Action> {
|
||||
match self.scene.node_at(at) {
|
||||
Some(id) if matches!(ctx.mode, Mode::Connect(_)) => vec![Action::cursor(id)],
|
||||
Some(id) => {
|
||||
let moved = CanvasOffset::default();
|
||||
self.drag = Some(Drag::Node { id, from: at, moved, group: Group::new() });
|
||||
@@ -264,6 +265,7 @@ impl View for CanvasView {
|
||||
mode,
|
||||
&[Context::Canvas, Context::Normal, Context::Motions],
|
||||
&[Context::Visual, Context::Canvas, Context::Motions],
|
||||
&[Context::Connect, Context::Canvas, Context::Motions],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -311,7 +313,7 @@ impl View for CanvasView {
|
||||
Mode::Move(grab) => return self.move_command(typed, grab, ctx),
|
||||
// Keys inside a title are the edit's, not the view's.
|
||||
Mode::Text(_) | Mode::Popup(_) => return None,
|
||||
Mode::Normal | Mode::Visual(_) | Mode::Command(_) => {}
|
||||
Mode::Normal | Mode::Visual(_) | Mode::Connect(_) | Mode::Command(_) => {}
|
||||
}
|
||||
let steps = cells(typed);
|
||||
let half_page = i32::from(self.area.height / 2);
|
||||
@@ -348,7 +350,7 @@ impl View for CanvasView {
|
||||
}
|
||||
Gesture::Press { at, .. } => {
|
||||
let at = self.screen(at);
|
||||
let Mode::Text(edit) = ctx.mode else { return self.press(at) };
|
||||
let Mode::Text(edit) = ctx.mode else { return self.press(at, ctx) };
|
||||
// While a title is edited, a click in it moves the text
|
||||
// cursor; a click anywhere else keeps the edit, finishes it,
|
||||
// and does what a click does.
|
||||
@@ -356,7 +358,7 @@ impl View for CanvasView {
|
||||
return vec![Action::App(AppAction::TextCursor(offset))];
|
||||
}
|
||||
let mut actions = edit.finish(ctx);
|
||||
actions.extend(self.press(at));
|
||||
actions.extend(self.press(at, ctx));
|
||||
actions
|
||||
}
|
||||
Gesture::DoublePress { at } => {
|
||||
|
||||
+75
-6
@@ -15,17 +15,22 @@ pub mod note;
|
||||
pub mod outline;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
|
||||
use crate::app::action::Action;
|
||||
use crate::app::mode::Mode;
|
||||
use crate::app::portals::{Portals, Shown};
|
||||
use crate::input::bindings::Context;
|
||||
use crate::input::engine::Typed;
|
||||
use crate::input::mouse::Gesture;
|
||||
use crate::model::{CanvasRect, CanvasSize, Document, Node, NodeId};
|
||||
use crate::ui::theme::Theme;
|
||||
use crate::model::{CanvasRect, CanvasSize, Content, Document, LinkPath, Node, NodeId, Portal};
|
||||
use crate::storage::link_paths::resolve;
|
||||
use crate::storage::names::title_from_file;
|
||||
use crate::ui::theme::{NodeStyle, Theme};
|
||||
use canvas::Camera;
|
||||
use canvas::boxes::NodeBox;
|
||||
|
||||
@@ -80,15 +85,68 @@ pub struct ViewContext<'a> {
|
||||
pub mode: &'a Mode,
|
||||
pub theme: &'a Theme,
|
||||
pub settings: &'a ViewSettings,
|
||||
/// What the documents portals lead to look like.
|
||||
pub portals: &'a Portals,
|
||||
/// The folder of the document's file: portal paths start there.
|
||||
pub folder: &'a Path,
|
||||
}
|
||||
|
||||
impl ViewContext<'_> {
|
||||
impl<'a> ViewContext<'a> {
|
||||
/// Every node's box on the canvas, at Normal zoom. Every view needs
|
||||
/// these to place new nodes, since every node has a spot on the canvas.
|
||||
pub fn box_rects(&self) -> BTreeMap<NodeId, CanvasRect> {
|
||||
canvas::boxes::box_rects(self.doc, self.settings.max_node_width, self.theme.glyphs)
|
||||
}
|
||||
|
||||
/// How a node is drawn, worked out by the theme. A portal without a
|
||||
/// color of its own takes its document's, and fades when its document
|
||||
/// isn't there.
|
||||
pub fn node_style(&self, id: NodeId, node: &Node) -> NodeStyle {
|
||||
let mut style = self.theme.node_style(node, id == self.doc.root());
|
||||
if let Content::Portal(portal) = &node.content {
|
||||
match self.portal(portal) {
|
||||
Shown::Live(seen) if style.color == Color::Reset => {
|
||||
style.color = self.theme.node_style(&seen.root, true).color;
|
||||
}
|
||||
Shown::Missing | Shown::Other(_) => style.faded = true,
|
||||
Shown::Live(_) | Shown::Unknown => {}
|
||||
}
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
/// What a node's box says in its bottom right corner, besides what a
|
||||
/// fold hides: on the root, how many portals lead to the document (`«
|
||||
/// 2`); on a portal, how many nodes its document has (`?` when it isn't
|
||||
/// there).
|
||||
pub fn badge(&self, id: NodeId, node: &Node) -> Option<String> {
|
||||
if let Content::Portal(portal) = &node.content {
|
||||
return match self.portal(portal) {
|
||||
Shown::Live(seen) => Some(seen.count.to_string()),
|
||||
Shown::Missing | Shown::Other(_) => Some("?".to_string()),
|
||||
Shown::Unknown => None,
|
||||
};
|
||||
}
|
||||
let backlinks = self.doc.backlinks().len();
|
||||
(id == self.doc.root() && backlinks > 0)
|
||||
.then(|| format!("{} {backlinks}", self.theme.glyphs.backlink()))
|
||||
}
|
||||
|
||||
/// What a portal of this document shows of its own document.
|
||||
pub fn portal(&self, portal: &Portal) -> Shown<'a> {
|
||||
self.portals.shown(self.folder, portal)
|
||||
}
|
||||
|
||||
/// The name of the document at a path a backlink keeps (relative to
|
||||
/// this document's folder): its title when it's been seen, or one made
|
||||
/// from its file name.
|
||||
pub fn document_named(&self, path: &LinkPath) -> String {
|
||||
let path = resolve(self.folder, path);
|
||||
self.portals
|
||||
.seen(&path)
|
||||
.map_or_else(|| title_from_file(&path).to_string(), |seen| seen.title().to_string())
|
||||
}
|
||||
|
||||
/// The size a node's box has on the canvas.
|
||||
pub fn box_size(&self, node: &Node) -> CanvasSize {
|
||||
NodeBox::new(node, String::new(), self.settings.max_node_width, self.theme.glyphs).rect.size
|
||||
@@ -96,16 +154,18 @@ impl ViewContext<'_> {
|
||||
}
|
||||
|
||||
/// The key tables a view reads in a mode: `normal` in normal mode (its own
|
||||
/// table first), `visual` in visual mode, and what every view reads in the
|
||||
/// other modes.
|
||||
/// table first), `visual` and `connect` in those modes, and what every view
|
||||
/// reads in the other modes.
|
||||
pub fn contexts_for(
|
||||
mode: &Mode,
|
||||
normal: &'static [Context],
|
||||
visual: &'static [Context],
|
||||
connect: &'static [Context],
|
||||
) -> &'static [Context] {
|
||||
match mode {
|
||||
Mode::Normal => normal,
|
||||
Mode::Visual(_) => visual,
|
||||
Mode::Connect(_) => connect,
|
||||
Mode::Text(edit) if edit.editor.is_typing() => &[Context::Insert],
|
||||
Mode::Text(_) => &[Context::Text],
|
||||
Mode::Move(_) => &[Context::Move],
|
||||
@@ -170,11 +230,20 @@ pub(crate) mod test_support {
|
||||
pub mode: Mode,
|
||||
pub theme: Theme,
|
||||
pub settings: ViewSettings,
|
||||
pub portals: Portals,
|
||||
}
|
||||
|
||||
impl Setup {
|
||||
pub fn ctx<'a>(&'a self, doc: &'a Document, cursor: NodeId) -> ViewContext<'a> {
|
||||
ViewContext { doc, cursor, mode: &self.mode, theme: &self.theme, settings: &self.settings }
|
||||
ViewContext {
|
||||
doc,
|
||||
cursor,
|
||||
mode: &self.mode,
|
||||
theme: &self.theme,
|
||||
settings: &self.settings,
|
||||
portals: &self.portals,
|
||||
folder: Path::new("."),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
-8
@@ -14,7 +14,8 @@
|
||||
//! ```
|
||||
//!
|
||||
//! It shows the cursor's node, and follows it. A note is made of parts: the
|
||||
//! title, the tags, the body, each link and each child. `j` and `k` go from
|
||||
//! title, the tags, the body, each link, each child, and on the root, each
|
||||
//! portal that leads to the document. `j` and `k` go from
|
||||
//! part to part (the bar on the left marks the one you're on), and Enter or
|
||||
//! `i` edits it, or goes to that link or child. The body is edited like a
|
||||
//! title, with Enter starting a new line. `h` and `l` turn the page: the
|
||||
@@ -27,16 +28,19 @@ use ratatui::layout::{Position, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
use crate::app::action::{Action, AppAction, Message};
|
||||
use crate::app::action::{self, Action, AppAction, Message};
|
||||
use crate::app::commands;
|
||||
use crate::app::jumps::Place;
|
||||
use crate::app::mode::Mode;
|
||||
use crate::app::popup::MenuFor;
|
||||
use crate::app::portals::Shown;
|
||||
use crate::app::text_edit::{EditAt, Field, StartEdit, TextEdit};
|
||||
use crate::input::bindings::Context;
|
||||
use crate::input::command::{Command, Motion};
|
||||
use crate::input::engine::Typed;
|
||||
use crate::input::mouse::{Cell, Gesture};
|
||||
use crate::model::{Content, Direction, NodeId};
|
||||
use crate::storage::link_paths::resolve;
|
||||
use crate::ui::text_editor::layout;
|
||||
use crate::view::{View, ViewContext, ViewKind, contexts_for};
|
||||
|
||||
@@ -56,6 +60,8 @@ enum Part {
|
||||
Link(usize),
|
||||
/// A child, in order.
|
||||
Child(usize),
|
||||
/// On the root: a portal in another document that leads here.
|
||||
Backlink(usize),
|
||||
}
|
||||
|
||||
/// The Note view.
|
||||
@@ -78,6 +84,8 @@ pub struct NoteView {
|
||||
links: Vec<NodeId>,
|
||||
/// The node's children, in order.
|
||||
children: Vec<NodeId>,
|
||||
/// On the root: where the portals that lead here are.
|
||||
backlinks: Vec<Place>,
|
||||
/// The part being edited, as drawn.
|
||||
editing: Option<Editing>,
|
||||
/// A preview (the search's): no bar marks a part.
|
||||
@@ -131,7 +139,7 @@ impl NoteView {
|
||||
let muted = Style::new().fg(theme.muted);
|
||||
|
||||
// The title, in the node's color.
|
||||
let color = theme.node_style(node, id == doc.root()).color;
|
||||
let color = ctx.node_style(id, node).color;
|
||||
let title_style = Style::new().fg(color).add_modifier(Modifier::BOLD);
|
||||
match (draft(Field::Title), &node.content) {
|
||||
(Some(edit), _) => self.add_draft(Part::Title, edit, title_style),
|
||||
@@ -139,8 +147,16 @@ impl NoteView {
|
||||
(None, Content::Portal(portal)) => {
|
||||
let title = format!("{} {}", glyphs.portal(), portal.title);
|
||||
self.add_text(Part::Title, &title, title_style);
|
||||
let path = format!("a portal to {}", portal.path.as_str());
|
||||
self.add_row(Some(Part::Title), vec![Span::styled(path, muted)]);
|
||||
let path = portal.path.as_str();
|
||||
let about = match ctx.portal(portal) {
|
||||
Shown::Live(seen) => {
|
||||
format!("a portal to {path} · {} · Enter goes there", action::nodes(seen.count))
|
||||
}
|
||||
Shown::Missing => format!("a portal to {path}, which isn't there"),
|
||||
Shown::Other(seen) => format!("a portal to {path}, which holds \"{}\" now", seen.title()),
|
||||
Shown::Unknown => format!("a portal to {path}"),
|
||||
};
|
||||
self.add_row(Some(Part::Title), vec![Span::styled(about, muted)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +231,23 @@ impl NoteView {
|
||||
vec![Span::raw(format!("{} {title}", glyphs.child_mark()))],
|
||||
);
|
||||
}
|
||||
|
||||
// On the root: the portals that lead to the document.
|
||||
self.backlinks.clear();
|
||||
if id == doc.root() && !doc.backlinks().is_empty() {
|
||||
self.add_row(None, Vec::new());
|
||||
self.add_row(None, vec![Span::styled("Portals here", muted)]);
|
||||
}
|
||||
for (index, backlink) in doc.backlinks().iter().enumerate().filter(|_| id == doc.root()) {
|
||||
let title = ctx.document_named(&backlink.from);
|
||||
let spans = vec![
|
||||
Span::raw(format!("{} {title}", glyphs.backlink())),
|
||||
Span::styled(format!(" {}", backlink.from.as_str()), muted),
|
||||
];
|
||||
self.add_row(Some(Part::Backlink(index)), spans);
|
||||
let path = resolve(ctx.folder, &backlink.from);
|
||||
self.backlinks.push(Place { uid: backlink.uid, path, node: backlink.node });
|
||||
}
|
||||
}
|
||||
|
||||
fn add_row(&mut self, part: Option<Part>, spans: Vec<Span<'static>>) {
|
||||
@@ -299,7 +332,7 @@ impl NoteView {
|
||||
Part::Title => return None,
|
||||
Part::Tags => StartEdit::Tags(at),
|
||||
Part::Body => StartEdit::Body { at, in_card: false },
|
||||
Part::Link(_) | Part::Child(_) if at != EditAt::End => {
|
||||
Part::Link(_) | Part::Child(_) | Part::Backlink(_) if at != EditAt::End => {
|
||||
let text = "Enter goes there: the title, the tags and the body are the parts you type in";
|
||||
return Some(vec![Action::say(Message::info(text))]);
|
||||
}
|
||||
@@ -310,6 +343,13 @@ impl NoteView {
|
||||
Part::Child(index) => {
|
||||
return Some(self.children.get(index).map(|&to| Action::cursor(to)).into_iter().collect());
|
||||
}
|
||||
// Up to the portal, in its document: a jump.
|
||||
Part::Backlink(index) => {
|
||||
let place = self.backlinks.get(index).cloned();
|
||||
return Some(
|
||||
place.map(|place| Action::App(AppAction::GoToPlace(place))).into_iter().collect(),
|
||||
);
|
||||
}
|
||||
};
|
||||
Some(vec![Action::App(AppAction::StartEdit(start))])
|
||||
}
|
||||
@@ -364,6 +404,7 @@ impl View for NoteView {
|
||||
mode,
|
||||
&[Context::Note, Context::Normal, Context::Motions],
|
||||
&[Context::Visual, Context::Motions],
|
||||
&[Context::Connect, Context::Motions],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -419,7 +460,7 @@ impl View for NoteView {
|
||||
fn command(&mut self, typed: &Typed, ctx: &ViewContext<'_>) -> Option<Vec<Action>> {
|
||||
match ctx.mode {
|
||||
Mode::Move(grab) => return commands::reorder(typed, grab, ctx),
|
||||
Mode::Normal | Mode::Visual(_) => {}
|
||||
Mode::Normal | Mode::Visual(_) | Mode::Connect(_) => {}
|
||||
Mode::Text(_) | Mode::Command(_) | Mode::Popup(_) => return None,
|
||||
}
|
||||
let times = isize::try_from(typed.times()).unwrap_or(1);
|
||||
@@ -487,7 +528,7 @@ impl View for NoteView {
|
||||
let start = StartEdit::Body { at: spot, in_card: false };
|
||||
Some(vec![Action::App(AppAction::StartEdit(start))])
|
||||
}
|
||||
Part::Link(_) | Part::Child(_) => self.open(EditAt::End),
|
||||
Part::Link(_) | Part::Child(_) | Part::Backlink(_) => self.open(EditAt::End),
|
||||
};
|
||||
actions.extend(open.unwrap_or_default());
|
||||
actions
|
||||
|
||||
+8
-2
@@ -35,6 +35,7 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::app::action::{Action, AppAction, Message};
|
||||
use crate::app::commands;
|
||||
use crate::app::connect;
|
||||
use crate::app::mode::Mode;
|
||||
use crate::app::popup::MenuFor;
|
||||
use crate::app::text_edit::{EditAt, StartEdit, TextEdit};
|
||||
@@ -124,6 +125,7 @@ impl OutlineView {
|
||||
let edit = ctx.mode.title_edit();
|
||||
self.picked = match ctx.mode {
|
||||
Mode::Visual(visual) => selection(visual, ctx, ViewKind::Outline).into_iter().collect(),
|
||||
Mode::Connect(connecting) => connect::others(connecting, ctx.cursor).into_iter().collect(),
|
||||
_ => BTreeSet::new(),
|
||||
};
|
||||
// The color of the lines from a node to its children, as on the
|
||||
@@ -173,7 +175,7 @@ impl OutlineView {
|
||||
let (doc, theme, glyphs) = (ctx.doc, ctx.theme, ctx.theme.glyphs);
|
||||
let Some(node) = doc.node(id) else { return };
|
||||
let top_level = lines.spans.is_empty();
|
||||
let style = theme.node_style(node, id == doc.root());
|
||||
let style = ctx.node_style(id, node);
|
||||
let color = style.color;
|
||||
let has_children = !doc.children(id).is_empty();
|
||||
let folded = visible.hidden.contains_key(&id);
|
||||
@@ -228,6 +230,9 @@ impl OutlineView {
|
||||
if has_body && !self.bodies {
|
||||
lines.push(format!(" {}", glyphs.body_mark()), muted);
|
||||
}
|
||||
if let Some(badge) = ctx.badge(id, node) {
|
||||
lines.push(format!(" {badge}"), muted);
|
||||
}
|
||||
if let Some(count) = visible.hidden.get(&id) {
|
||||
lines.push(format!(" +{count}"), muted);
|
||||
}
|
||||
@@ -433,6 +438,7 @@ impl View for OutlineView {
|
||||
mode,
|
||||
&[Context::Outline, Context::Normal, Context::Motions],
|
||||
&[Context::Visual, Context::Outline, Context::Motions],
|
||||
&[Context::Connect, Context::Outline, Context::Motions],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -475,7 +481,7 @@ impl View for OutlineView {
|
||||
fn command(&mut self, typed: &Typed, ctx: &ViewContext<'_>) -> Option<Vec<Action>> {
|
||||
match ctx.mode {
|
||||
Mode::Move(grab) => return commands::reorder(typed, grab, ctx),
|
||||
Mode::Normal | Mode::Visual(_) => {}
|
||||
Mode::Normal | Mode::Visual(_) | Mode::Connect(_) => {}
|
||||
Mode::Text(_) | Mode::Command(_) | Mode::Popup(_) => return None,
|
||||
}
|
||||
let rows = isize::try_from(typed.times()).unwrap_or(1);
|
||||
|
||||
Reference in new issue
Block a user