feat: Implement bidirectional Story Bible links with draft text
- Add story_bible.rs module with StoryBibleIndex, StoryBibleItem, and StoryBibleReference types for indexing Story Bible content - Support wiki-style [[...]] and mention-style @... syntax for references - Highlight Story Bible references in editor with purple background - Add Story Bible window (View > Tools > Story Bible) to browse items - Show references from current draft file in Story Bible window - Allow clicking references to jump to their location in the editor - Auto-index Story Bible items from common directories (03-Characters, 02-Story Bible, Characters, etc.) - Extract metadata from YAML frontmatter or filenames - Add walkdir dependency for directory scanning - Fix pre-existing format string errors in workspace.rs Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
+41
-1
@@ -43,6 +43,13 @@ impl App {
|
||||
.id_salt("editor")
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
// Prepare Story Bible reference ranges for highlighting
|
||||
let bible_ranges: Vec<(usize, usize)> = self
|
||||
.current_references
|
||||
.iter()
|
||||
.map(|r| (r.column, r.column + r.length))
|
||||
.collect();
|
||||
|
||||
let mut layouter = move |ui: &egui::Ui, text: &str, wrap_width: f32| {
|
||||
let job = build_editor_job(
|
||||
text,
|
||||
@@ -51,6 +58,7 @@ impl App {
|
||||
&ranges,
|
||||
&find_ranges,
|
||||
find_active,
|
||||
&bible_ranges,
|
||||
wrap_width,
|
||||
);
|
||||
ui.fonts(|f| f.layout_job(job))
|
||||
@@ -105,6 +113,8 @@ impl App {
|
||||
self.find_needs_refresh = true;
|
||||
self.spell_dirty = true;
|
||||
self.spell_last_edit = Some(Instant::now());
|
||||
// Update Story Bible references for the current file
|
||||
self.update_current_references();
|
||||
}
|
||||
// Markdown formatting hotkeys, applied to the current selection
|
||||
// while the editor is focused (Ctrl/Cmd + B / I / E / K, and
|
||||
@@ -358,6 +368,7 @@ impl App {
|
||||
/// get a red underline, grammar/style issues a blue one. `find` are
|
||||
/// `(start_byte, end_byte)` search-match ranges, shaded with a translucent
|
||||
/// highlight; the one at index `find_active` (if any) is shaded more strongly.
|
||||
/// `bible` are Story Bible reference ranges, highlighted with a distinctive color.
|
||||
pub(super) fn build_editor_job(
|
||||
text: &str,
|
||||
size: f32,
|
||||
@@ -365,6 +376,7 @@ pub(super) fn build_editor_job(
|
||||
grammar: &[(usize, usize, bool)],
|
||||
find: &[(usize, usize)],
|
||||
find_active: Option<usize>,
|
||||
bible: &[(usize, usize)],
|
||||
wrap_width: f32,
|
||||
) -> egui::text::LayoutJob {
|
||||
use egui::text::{LayoutJob, TextFormat};
|
||||
@@ -377,7 +389,7 @@ pub(super) fn build_editor_job(
|
||||
return job;
|
||||
}
|
||||
|
||||
// Split the text at every match boundary (grammar and find), then format
|
||||
// Split the text at every match boundary (grammar, find, and bible), then format
|
||||
// each run according to which ranges cover it.
|
||||
let mut points: Vec<usize> = vec![0, text.len()];
|
||||
for &(s, e, _) in grammar {
|
||||
@@ -388,6 +400,10 @@ pub(super) fn build_editor_job(
|
||||
points.push(s);
|
||||
points.push(e);
|
||||
}
|
||||
for &(s, e) in bible {
|
||||
points.push(s);
|
||||
points.push(e);
|
||||
}
|
||||
points.retain(|&p| p <= text.len() && text.is_char_boundary(p));
|
||||
points.sort_unstable();
|
||||
points.dedup();
|
||||
@@ -397,6 +413,8 @@ pub(super) fn build_editor_job(
|
||||
// Translucent so the highlight reads in both light and dark themes.
|
||||
let find_bg = egui::Color32::from_rgba_unmultiplied(255, 213, 0, 70);
|
||||
let find_active_bg = egui::Color32::from_rgba_unmultiplied(255, 145, 0, 150);
|
||||
// Story Bible reference highlight - purple/blue color
|
||||
let bible_bg = egui::Color32::from_rgba_unmultiplied(138, 43, 226, 70);
|
||||
let width = (size * 0.08).max(1.5);
|
||||
|
||||
for w in points.windows(2) {
|
||||
@@ -421,6 +439,12 @@ pub(super) fn build_editor_job(
|
||||
find_bg
|
||||
};
|
||||
}
|
||||
// Check for Story Bible references and apply background highlight
|
||||
// Only apply if not already highlighted by find
|
||||
let is_find_highlight = find.iter().any(|&(s, e)| s < e && a >= s && b <= e);
|
||||
if !is_find_highlight && bible.iter().any(|&(s, e)| s < e && a >= s && b <= e) {
|
||||
fmt.background = bible_bg;
|
||||
}
|
||||
job.append(&text[a..b], 0.0, fmt);
|
||||
}
|
||||
job
|
||||
@@ -825,3 +849,19 @@ mod tests {
|
||||
assert_eq!(&text[char_to_byte(&text, sel.start)..char_to_byte(&text, sel.end)], "word");
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Update the current_references list with Story Bible references found in the buffer.
|
||||
pub(super) fn update_current_references(&mut self) {
|
||||
if let Some(idx) = self.selected {
|
||||
let file_path = self.files[idx].clone();
|
||||
let mut refs = crate::story_bible::find_references(&self.buffer, &self.story_bible_index);
|
||||
for r in refs.iter_mut() {
|
||||
r.file_path = file_path.clone();
|
||||
}
|
||||
self.current_references = refs;
|
||||
} else {
|
||||
self.current_references.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user