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:
@@ -0,0 +1,99 @@
|
||||
//! Comparing the open file with its last committed version.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl App {
|
||||
/// Load the diff for the current file, ready for the diff window.
|
||||
pub(super) fn open_diff(&mut self) {
|
||||
self.save_current();
|
||||
let Some(name) = self.selected.and_then(|i| self.files.get(i)).cloned() else {
|
||||
self.status = "Open a file first".to_string();
|
||||
return;
|
||||
};
|
||||
// Git is run from the work tree, and the workspace may be below it.
|
||||
let root = self.repo_root.clone();
|
||||
let Some(root) = root else {
|
||||
self.diff_text = "This workspace is not in a git repository.".to_string();
|
||||
self.show_diff = true;
|
||||
return;
|
||||
};
|
||||
if !gitsync::has_commits(&root) {
|
||||
self.diff_text = "The repository has no commits yet, so there is \
|
||||
nothing to compare against."
|
||||
.to_string();
|
||||
self.show_diff = true;
|
||||
return;
|
||||
}
|
||||
let abs = self.path_for(&name);
|
||||
let rel = abs
|
||||
.strip_prefix(&root)
|
||||
.map(|p| p.to_string_lossy().replace('\\', "/"))
|
||||
.unwrap_or_else(|_| name.clone());
|
||||
self.diff_text = match gitsync::diff_file(&root, &rel) {
|
||||
Ok(text) if text.trim().is_empty() => {
|
||||
format!("{name} matches the last commit — no changes.")
|
||||
}
|
||||
Ok(text) => text,
|
||||
Err(e) => format!("Could not diff {name}:\n{e}"),
|
||||
};
|
||||
self.diff_title = name;
|
||||
self.show_diff = true;
|
||||
}
|
||||
|
||||
/// The diff window: added and removed lines since the last commit.
|
||||
pub(super) fn diff_window(&mut self, ctx: &egui::Context) {
|
||||
let mut open = self.show_diff;
|
||||
let mut refresh = false;
|
||||
egui::Window::new(format!("Changes — {}", self.diff_title))
|
||||
.open(&mut open)
|
||||
.resizable(true)
|
||||
.collapsible(false)
|
||||
.default_width(680.0)
|
||||
.default_height(460.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⟳ Refresh").clicked() {
|
||||
refresh = true;
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new("compared with the last commit")
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
});
|
||||
ui.separator();
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
for line in self.diff_text.lines() {
|
||||
// Colour the diff the way git does, so additions and
|
||||
// removals are separable at a glance.
|
||||
let (color, strong) = match line.as_bytes().first() {
|
||||
Some(b'+') if !line.starts_with("+++") => {
|
||||
(Some(egui::Color32::from_rgb(0x3F, 0x9E, 0x4F)), false)
|
||||
}
|
||||
Some(b'-') if !line.starts_with("---") => {
|
||||
(Some(egui::Color32::from_rgb(0xC0, 0x50, 0x50)), false)
|
||||
}
|
||||
Some(b'@') => {
|
||||
(Some(egui::Color32::from_rgb(0x3B, 0x82, 0xF6)), true)
|
||||
}
|
||||
_ => (None, false),
|
||||
};
|
||||
let mut text = egui::RichText::new(line).monospace();
|
||||
if let Some(color) = color {
|
||||
text = text.color(color);
|
||||
}
|
||||
if strong {
|
||||
text = text.strong();
|
||||
}
|
||||
ui.label(text);
|
||||
}
|
||||
});
|
||||
});
|
||||
if refresh {
|
||||
self.open_diff();
|
||||
}
|
||||
self.show_diff = open;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user