Add workspace search, synopsis view, snapshot diffs, typewriter mode, and a writing streak
Five productivity features: search/replace across the whole workspace (with a safety commit before Replace all), a Synopsis window compiling every chapter's slug, a Diff button per Auto-Snapshot to compare against an older snapshot instead of only HEAD, typewriter-style cursor centering and paragraph dimming gated on Distraction-free mode, and a per-day word-count streak persisted to word_log.json alongside order.json/titles.json. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+58
-2
@@ -31,9 +31,11 @@ mod spelling;
|
||||
mod story_bible_editor;
|
||||
mod story_bible_ui;
|
||||
mod style;
|
||||
mod synopsis;
|
||||
mod ui;
|
||||
mod util;
|
||||
mod workspace;
|
||||
mod workspace_search;
|
||||
|
||||
// Pulled up so the sibling modules reach these free helpers through their own
|
||||
// `use super::*;`. Modules whose helpers are only used locally aren't listed.
|
||||
@@ -189,6 +191,10 @@ pub struct App {
|
||||
/// Per-file chapter title overrides (file name -> title). Missing/empty means
|
||||
/// the title is derived automatically at export time.
|
||||
titles: HashMap<String, String>,
|
||||
/// Per-day manuscript word-count log, for the writing streak in the
|
||||
/// status bar. Lives in the workspace (`word_log.json`) and syncs with
|
||||
/// git the same way `titles`/`order.json` do.
|
||||
word_log: crate::word_log::WordLog,
|
||||
/// Editing buffer for the selected file's chapter title override.
|
||||
title_input: String,
|
||||
selected: Option<usize>,
|
||||
@@ -198,6 +204,11 @@ pub struct App {
|
||||
/// The editorial header lifted out of `buffer` while it is collapsed.
|
||||
header_stash: Option<String>,
|
||||
dirty: bool,
|
||||
/// Byte offset of the primary cursor in `buffer`, as of the last frame.
|
||||
/// Used by typewriter mode to notice the cursor moved (and re-center it)
|
||||
/// versus the writer scrolling the view manually, and to find the current
|
||||
/// paragraph to keep in focus.
|
||||
cursor_pos: usize,
|
||||
/// Editable copy of the workspace path shown in the top bar.
|
||||
workspace_input: String,
|
||||
/// Editable copy of the export path.
|
||||
@@ -283,6 +294,19 @@ pub struct App {
|
||||
find_focus: Option<bool>,
|
||||
/// Request the editor to scroll the active match into view next frame.
|
||||
find_scroll: bool,
|
||||
/// Whether the workspace-wide search window is visible.
|
||||
show_workspace_search: bool,
|
||||
/// The workspace search text.
|
||||
ws_query: String,
|
||||
/// The workspace replacement text.
|
||||
ws_replace: String,
|
||||
/// Whether the workspace search is case-sensitive.
|
||||
ws_case_sensitive: bool,
|
||||
/// The last workspace search's results: one entry per file with at least
|
||||
/// one match — its name, total match count, and a few line snippets.
|
||||
ws_results: Vec<(String, usize, Vec<String>)>,
|
||||
/// One-line status for the workspace search window.
|
||||
ws_status: String,
|
||||
/// Active header-field autocomplete popup, if any.
|
||||
autocomplete: Option<Autocomplete>,
|
||||
/// A partial the user dismissed with Esc; the popup stays closed until the
|
||||
@@ -321,6 +345,8 @@ pub struct App {
|
||||
show_manuscript_settings: bool,
|
||||
/// Whether the Outline window is open.
|
||||
show_outline: bool,
|
||||
/// Whether the Synopsis window is open.
|
||||
show_synopsis: bool,
|
||||
/// Whether to open an outline file from a folder context menu.
|
||||
/// Contains the workspace-relative path of the outline file to open.
|
||||
open_outline_file: Option<String>,
|
||||
@@ -328,6 +354,12 @@ pub struct App {
|
||||
show_diff: bool,
|
||||
diff_text: String,
|
||||
diff_title: String,
|
||||
/// Commit the diff window is comparing against. `None` means the last
|
||||
/// commit (`HEAD`); `Some(hash)` means an older Auto-Snapshot picked from
|
||||
/// the Snapshots window.
|
||||
diff_base: Option<String>,
|
||||
/// Human-readable label for `diff_base`, shown in the diff window's header.
|
||||
diff_base_label: String,
|
||||
/// Text field for adding a word by hand.
|
||||
wordlist_input: String,
|
||||
/// Index (into the currently displayed matches) of the word a right-click
|
||||
@@ -424,11 +456,13 @@ impl App {
|
||||
files: Vec::new(),
|
||||
collapsed: HashSet::new(),
|
||||
titles: HashMap::new(),
|
||||
word_log: crate::word_log::WordLog::default(),
|
||||
title_input: String::new(),
|
||||
selected: None,
|
||||
buffer: String::new(),
|
||||
header_stash: None,
|
||||
dirty: false,
|
||||
cursor_pos: 0,
|
||||
new_name: String::new(),
|
||||
rename_input: String::new(),
|
||||
pending_delete: false,
|
||||
@@ -467,6 +501,12 @@ impl App {
|
||||
find_needs_refresh: false,
|
||||
find_focus: None,
|
||||
find_scroll: false,
|
||||
show_workspace_search: false,
|
||||
ws_query: String::new(),
|
||||
ws_replace: String::new(),
|
||||
ws_case_sensitive: false,
|
||||
ws_results: Vec::new(),
|
||||
ws_status: String::new(),
|
||||
autocomplete: None,
|
||||
ac_dismissed: None,
|
||||
field_names: Vec::new(),
|
||||
@@ -485,10 +525,13 @@ impl App {
|
||||
show_characters: false,
|
||||
show_manuscript_settings: false,
|
||||
show_outline: false,
|
||||
show_synopsis: false,
|
||||
open_outline_file: None,
|
||||
show_diff: false,
|
||||
diff_text: String::new(),
|
||||
diff_title: String::new(),
|
||||
diff_base: None,
|
||||
diff_base_label: "the last commit".to_string(),
|
||||
wordlist_input: String::new(),
|
||||
spell_menu: None,
|
||||
dismissed: HashSet::new(),
|
||||
@@ -555,10 +598,16 @@ impl eframe::App for App {
|
||||
self.toggle_distraction_free();
|
||||
}
|
||||
|
||||
// Ctrl+F opens find (search focused); Ctrl+H opens it focused on replace.
|
||||
if ctx.input(|i| i.modifiers.command && i.key_pressed(egui::Key::F)) {
|
||||
// Ctrl+F opens find (search focused); Ctrl+Shift+F searches the whole
|
||||
// workspace instead; Ctrl+H opens find focused on replace.
|
||||
if ctx.input(|i| i.modifiers.command && !i.modifiers.shift && i.key_pressed(egui::Key::F))
|
||||
{
|
||||
self.open_find(true);
|
||||
}
|
||||
if ctx.input(|i| i.modifiers.command && i.modifiers.shift && i.key_pressed(egui::Key::F))
|
||||
{
|
||||
self.open_workspace_search();
|
||||
}
|
||||
if ctx.input(|i| i.modifiers.command && i.key_pressed(egui::Key::H)) {
|
||||
self.open_find(false);
|
||||
}
|
||||
@@ -676,6 +725,13 @@ impl eframe::App for App {
|
||||
if self.show_outline {
|
||||
self.outline_window(ctx);
|
||||
}
|
||||
if self.show_synopsis {
|
||||
self.synopsis_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_workspace_search {
|
||||
self.workspace_search_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_diff {
|
||||
self.diff_window(ctx);
|
||||
|
||||
Reference in New Issue
Block a user