Add project-mode tooling: characters, outline, diff and revision status
Rounds out project mode, where the workspace is the project root and one subfolder holds the manuscript proper: - Characters and Outline windows, backed by new `characters` and `outline` modules that read the cast from character sheets and measure how much of the snowflake outline is actually written. - Edit ▸ Changes… diffs the open file against its last committed version. - Revision status, per-file and project word counts, an archive action and hidden folders in the file panel. - Chapter-file export alongside the ODT master, richer header parsing, and a project word list for names and invented terms. - The export path now follows the workspace: opening a project points it at that project root, keeping a file name you chose yourself and re-deriving one that merely echoed the folder it sat in. - Clicking an issue in the grammar/spelling panel takes the editor to it, selecting the words and centring them; applying a suggestion jumps to the rewritten text as well. README covers the new windows and workflows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bSn3Xijp8GofZVUnRX4oq
This commit is contained in:
+73
-6
@@ -16,9 +16,12 @@ use std::time::{Duration, Instant};
|
||||
|
||||
mod autocomplete;
|
||||
mod beats;
|
||||
mod characters;
|
||||
mod diff;
|
||||
mod editor;
|
||||
mod file_list;
|
||||
mod find;
|
||||
mod outline;
|
||||
mod project;
|
||||
mod grammar;
|
||||
mod spelling;
|
||||
@@ -33,6 +36,7 @@ use self::autocomplete::*;
|
||||
use self::file_list::*;
|
||||
use self::grammar::*;
|
||||
use self::project::*;
|
||||
use self::spelling::*;
|
||||
use self::style::*;
|
||||
use self::util::*;
|
||||
|
||||
@@ -70,6 +74,8 @@ struct FileMeta {
|
||||
goal: Option<crate::preprocess::WordGoal>,
|
||||
/// Prose (body) word count captured with the rest of this metadata.
|
||||
prose_words: usize,
|
||||
/// The `Status:` line, if any — the revision stage this file has reached.
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
impl FileMeta {
|
||||
@@ -81,14 +87,10 @@ impl FileMeta {
|
||||
pov: h.pov,
|
||||
goal: h.goal,
|
||||
prose_words: count_words(&h.body),
|
||||
status: crate::preprocess::field(text, marker, "Status"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this entry carries anything worth caching/showing.
|
||||
fn has_display(&self) -> bool {
|
||||
self.slug.is_some() || self.pov.is_some() || self.goal.is_some()
|
||||
}
|
||||
|
||||
/// The tooltip text — a `POV:` line then the slug synopsis — or `None` when
|
||||
/// the file carries neither field.
|
||||
fn tooltip(&self) -> Option<String> {
|
||||
@@ -163,6 +165,10 @@ struct IssueItem {
|
||||
|
||||
pub struct App {
|
||||
config: Config,
|
||||
/// The manuscript folder within the workspace when the workspace is a
|
||||
/// project root — files under it are the book, everything else is
|
||||
/// reference. `None` when the workspace is itself the manuscript.
|
||||
manuscript_dir: Option<String>,
|
||||
/// Ordered markdown files, as workspace-relative paths with `/` separators
|
||||
/// (`part-1/ch-03.md`). Always held in folder-tree order — see
|
||||
/// [`crate::order::tree_order`] — so this list reads exactly as the file
|
||||
@@ -177,8 +183,11 @@ pub struct App {
|
||||
/// Editing buffer for the selected file's chapter title override.
|
||||
title_input: String,
|
||||
selected: Option<usize>,
|
||||
/// Editor contents for the selected file.
|
||||
/// Editor contents for the selected file. While the header is collapsed
|
||||
/// this is the prose only; [`App::document`] puts the file back together.
|
||||
buffer: String,
|
||||
/// The editorial header lifted out of `buffer` while it is collapsed.
|
||||
header_stash: Option<String>,
|
||||
dirty: bool,
|
||||
/// Editable copy of the workspace path shown in the top bar.
|
||||
workspace_input: String,
|
||||
@@ -268,9 +277,33 @@ pub struct App {
|
||||
spell_rx: Option<std::sync::mpsc::Receiver<SpellCheckMsg>>,
|
||||
/// One-line status for the spell checker (dictionary name, count, or error).
|
||||
spell_status: String,
|
||||
/// Names and invented terms the checker accepts on top of the dictionary,
|
||||
/// loaded from the workspace's `wordlist.json`.
|
||||
wordlist: crate::spell::WordList,
|
||||
/// Whether the word-list window is open.
|
||||
show_wordlist: bool,
|
||||
/// When set, the file panel shows only files whose `Status:` matches.
|
||||
status_filter: Option<String>,
|
||||
/// The project's cast, read from its character sheets.
|
||||
characters: Vec<crate::characters::Character>,
|
||||
/// Whether the Characters window is open.
|
||||
show_characters: bool,
|
||||
/// Whether the manuscript-details window is open.
|
||||
show_manuscript_settings: bool,
|
||||
/// Whether the Outline window is open.
|
||||
show_outline: bool,
|
||||
/// Whether the diff window is open, and what it is showing.
|
||||
show_diff: bool,
|
||||
diff_text: String,
|
||||
diff_title: String,
|
||||
/// Text field for adding a word by hand.
|
||||
wordlist_input: String,
|
||||
/// Index (into the currently displayed matches) of the word a right-click
|
||||
/// suggestion menu is open for, if any.
|
||||
spell_menu: Option<usize>,
|
||||
/// Byte range in `buffer` the editor should select and scroll into view next
|
||||
/// frame, set when an issue is clicked in the results panel.
|
||||
issue_jump: Option<(usize, usize)>,
|
||||
/// Whether the Mistral settings window is open.
|
||||
show_mistral_settings: bool,
|
||||
/// Whether the new-file template settings window is open.
|
||||
@@ -312,12 +345,14 @@ impl App {
|
||||
workspace_input: config.workspace.display().to_string(),
|
||||
export_input: config.export_path.display().to_string(),
|
||||
config,
|
||||
manuscript_dir: None,
|
||||
files: Vec::new(),
|
||||
collapsed: HashSet::new(),
|
||||
titles: HashMap::new(),
|
||||
title_input: String::new(),
|
||||
selected: None,
|
||||
buffer: String::new(),
|
||||
header_stash: None,
|
||||
dirty: false,
|
||||
new_name: String::new(),
|
||||
rename_input: String::new(),
|
||||
@@ -360,7 +395,19 @@ impl App {
|
||||
spell_last_edit: None,
|
||||
spell_rx: None,
|
||||
spell_status: String::new(),
|
||||
wordlist: crate::spell::WordList::default(),
|
||||
show_wordlist: false,
|
||||
status_filter: None,
|
||||
characters: Vec::new(),
|
||||
show_characters: false,
|
||||
show_manuscript_settings: false,
|
||||
show_outline: false,
|
||||
show_diff: false,
|
||||
diff_text: String::new(),
|
||||
diff_title: String::new(),
|
||||
wordlist_input: String::new(),
|
||||
spell_menu: None,
|
||||
issue_jump: None,
|
||||
show_mistral_settings: false,
|
||||
show_template_settings: false,
|
||||
beats_rx: None,
|
||||
@@ -471,6 +518,26 @@ impl eframe::App for App {
|
||||
self.beats_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_wordlist {
|
||||
self.wordlist_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_characters {
|
||||
self.characters_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_manuscript_settings {
|
||||
self.manuscript_settings_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_outline {
|
||||
self.outline_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_diff {
|
||||
self.diff_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_cheatsheet {
|
||||
crate::help::cheatsheet_window(ctx, &mut self.show_cheatsheet);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user