Include the POV field in the file-list hover tooltip

Header parsing now also lifts a `POV:` line (any heading level). The
file-list tooltip combines it with the slug — "POV: <name>" then the
slug synopsis — cached per file alongside the slug (HoverInfo) and read
live from the buffer for the selected file. Adds a POV parse test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
This commit is contained in:
landon
2026-08-14 06:49:53 -05:00
parent 9c4498a680
commit c926b6696f
3 changed files with 84 additions and 34 deletions
+63 -32
View File
@@ -28,6 +28,43 @@ enum IssueSource {
None,
}
/// Cached header fields shown when hovering a file in the list.
#[derive(Default, Clone)]
struct HoverInfo {
/// The `# Slug:` synopsis line, if any.
slug: Option<String>,
/// The `POV:` line, if any.
pov: Option<String>,
}
impl HoverInfo {
/// Extract the hover fields from a document's header.
fn from_markdown(text: &str, marker: &str) -> Self {
let h = crate::preprocess::parse(text, marker);
HoverInfo {
slug: h.slug,
pov: h.pov,
}
}
fn is_empty(&self) -> bool {
self.slug.is_none() && self.pov.is_none()
}
/// The tooltip text — a `POV:` line then the slug synopsis — or `None` when
/// the file carries neither field.
fn tooltip(&self) -> Option<String> {
let mut lines = Vec::new();
if let Some(pov) = &self.pov {
lines.push(format!("POV: {pov}"));
}
if let Some(slug) = &self.slug {
lines.push(slug.clone());
}
(!lines.is_empty()).then(|| lines.join("\n"))
}
}
/// An owned snapshot of one issue for the results panel, decoupled from `self`
/// so the panel can render without holding a borrow across its click handling.
struct IssueItem {
@@ -74,10 +111,10 @@ pub struct App {
/// Each file's word count captured when the workspace was opened, used as the
/// per-file baseline for the "this session" delta.
session_start_counts: HashMap<String, usize>,
/// Cached `# Slug:` value per file (name -> slug) for the file-list hover
/// Cached header fields per file (name -> slug/POV) for the file-list hover
/// tooltip. Filled when the workspace opens and refreshed on save; the
/// selected file is read live from the buffer instead.
slugs: HashMap<String, String>,
hover_info: HashMap<String, HoverInfo>,
/// Grammar/spelling issues from the last LanguageTool check.
lt_matches: Vec<crate::langtool::Match>,
/// The exact buffer text the current `lt_matches` were computed against;
@@ -163,7 +200,7 @@ impl App {
repo_root: None,
pending_repo: None,
session_start_counts: HashMap::new(),
slugs: HashMap::new(),
hover_info: HashMap::new(),
lt_matches: Vec::new(),
lt_checked_text: String::new(),
lt_status: String::new(),
@@ -221,7 +258,7 @@ impl App {
self.pending_delete = false;
self.clear_lt();
self.session_start_counts = self.snapshot_counts();
self.slugs = self.snapshot_slugs();
self.hover_info = self.snapshot_hover_info();
if !self.files.is_empty() {
self.select(0);
}
@@ -295,15 +332,15 @@ impl App {
.collect()
}
/// Read every file and return its `# Slug:` header value (only for files that
/// have one), for the file-list hover tooltip.
fn snapshot_slugs(&self) -> HashMap<String, String> {
/// Read every file and return its hover fields (slug/POV), for files that
/// carry at least one, for the file-list tooltip.
fn snapshot_hover_info(&self) -> HashMap<String, HoverInfo> {
self.files
.iter()
.filter_map(|name| {
let text = std::fs::read_to_string(self.path_for(name)).unwrap_or_default();
let header = crate::preprocess::parse(&text, &self.config.draft_marker);
header.slug.map(|slug| (name.clone(), slug))
let info = HoverInfo::from_markdown(&text, &self.config.draft_marker);
(!info.is_empty()).then(|| (name.clone(), info))
})
.collect()
}
@@ -346,19 +383,13 @@ impl App {
Ok(_) => {
self.dirty = false;
self.status = format!("Saved {name}");
// Keep the hover-tooltip slug cache in step.
let slug = crate::preprocess::parse(
&self.buffer,
&self.config.draft_marker,
)
.slug;
match slug {
Some(s) => {
self.slugs.insert(name, s);
}
None => {
self.slugs.remove(&name);
}
// Keep the hover-tooltip cache (slug/POV) in step.
let info =
HoverInfo::from_markdown(&self.buffer, &self.config.draft_marker);
if info.is_empty() {
self.hover_info.remove(&name);
} else {
self.hover_info.insert(name, info);
}
}
Err(e) => self.status = format!("Save failed: {e}"),
@@ -445,7 +476,7 @@ impl App {
self.files.remove(idx);
self.titles.remove(&name);
self.session_start_counts.remove(&name);
self.slugs.remove(&name);
self.hover_info.remove(&name);
self.persist_titles();
self.persist_order();
self.selected = None;
@@ -497,8 +528,8 @@ impl App {
if let Some(words) = self.session_start_counts.remove(&old_name) {
self.session_start_counts.insert(new_name.clone(), words);
}
if let Some(slug) = self.slugs.remove(&old_name) {
self.slugs.insert(new_name.clone(), slug);
if let Some(info) = self.hover_info.remove(&old_name) {
self.hover_info.insert(new_name.clone(), info);
}
self.persist_order();
self.status = format!("Renamed to {new_name}");
@@ -1325,14 +1356,14 @@ impl App {
for idx in 0..self.files.len() {
let name = self.files[idx].clone();
let selected = self.selected == Some(idx);
// The selected file's slug is read live from the
// The selected file's fields are read live from the
// buffer (so unsaved edits show); others come from the
// cache filled on open/save.
let slug = if selected {
crate::preprocess::parse(&self.buffer, &self.config.draft_marker)
.slug
let tooltip = if selected {
HoverInfo::from_markdown(&self.buffer, &self.config.draft_marker)
.tooltip()
} else {
self.slugs.get(&name).cloned()
self.hover_info.get(&name).and_then(HoverInfo::tooltip)
};
let row = ui
.horizontal(|ui| {
@@ -1349,8 +1380,8 @@ impl App {
[ui.available_width(), 20.0],
egui::SelectableLabel::new(selected, &name),
);
if let Some(slug) = &slug {
label = label.on_hover_text(slug);
if let Some(tooltip) = &tooltip {
label = label.on_hover_text(tooltip);
}
if label.clicked() {
clicked = Some(idx);