From 223d046a675ff24fb9b03d12e0e3b22d92df315a Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Tue, 17 Jun 2025 14:58:06 +0000 Subject: [PATCH 1/2] Use strip_prefix instead of `starts_with` and indexing --- crates/rust-project-goals/src/markwaydown.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/rust-project-goals/src/markwaydown.rs b/crates/rust-project-goals/src/markwaydown.rs index c4f09bae..bec72c2f 100644 --- a/crates/rust-project-goals/src/markwaydown.rs +++ b/crates/rust-project-goals/src/markwaydown.rs @@ -163,8 +163,10 @@ fn categorize_line(line: &str) -> CategorizeLine { if line.starts_with('#') { let level = line.chars().take_while(|&ch| ch == '#').count(); CategorizeLine::Title(level, line.trim_start_matches('#').trim().to_string()) - } else if line.starts_with('|') && line.ends_with('|') { - let line = &line[1..line.len() - 1]; + } else if let Some(line) = line + .strip_prefix('|') + .and_then(|line| line.strip_suffix('|')) + { let columns = line.split('|').map(|s| s.trim()); if columns.clone().all(|s| s.chars().all(|c| c == '-')) { CategorizeLine::TableDashRow(columns.count()) From 58f3f70f10a7819f8140ac40b4644c453893932f Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 16 Jun 2025 13:57:33 +0000 Subject: [PATCH 2/2] Add span tracking to allow emitting diagnostics everywhere --- Cargo.lock | 113 ++++++++++++++- crates/mdbook-goals/Cargo.toml | 1 + .../mdbook-goals/src/mdbook_preprocessor.rs | 15 +- crates/rust-project-goals-cli/Cargo.toml | 1 + crates/rust-project-goals-cli/src/rfc.rs | 2 +- crates/rust-project-goals-cli/src/updates.rs | 7 +- crates/rust-project-goals/Cargo.toml | 1 + .../rust-project-goals/src/format_team_ask.rs | 13 +- crates/rust-project-goals/src/goal.rs | 78 ++++++---- crates/rust-project-goals/src/markwaydown.rs | 137 ++++++++---------- crates/rust-project-goals/src/util.rs | 11 +- 11 files changed, 255 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 126fe1ed..5f6c6928 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "atomic-waker" @@ -339,6 +339,33 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -587,6 +614,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1112,6 +1149,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "2.7.1" @@ -1313,6 +1356,7 @@ dependencies = [ "rust-project-goals", "semver", "serde_json", + "spanned", ] [[package]] @@ -1564,6 +1608,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owo-colors" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1984,6 +2034,7 @@ dependencies = [ "rust_team_data", "serde", "serde_json", + "spanned", "toml 0.8.19", "walkdir", ] @@ -2003,6 +2054,7 @@ dependencies = [ "rust-project-goals-json", "serde", "serde_json", + "spanned", "walkdir", ] @@ -2227,6 +2279,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -2280,6 +2341,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spanned" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8e76fd7aeee255ddaa885b9de506484b21cbd458ae9d04d1b24851e82ee58a" +dependencies = [ + "anyhow", + "bstr", + "color-eyre", +] + [[package]] name = "spin" version = "0.9.8" @@ -2441,6 +2513,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.36" @@ -2654,6 +2735,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -2764,6 +2867,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/crates/mdbook-goals/Cargo.toml b/crates/mdbook-goals/Cargo.toml index fc0864c0..e3640dfc 100644 --- a/crates/mdbook-goals/Cargo.toml +++ b/crates/mdbook-goals/Cargo.toml @@ -11,3 +11,4 @@ regex = "1.11.1" rust-project-goals = { version = "0.1.0", path = "../rust-project-goals" } semver = "1.0.23" serde_json = "1.0.133" +spanned = "0.4.0" diff --git a/crates/mdbook-goals/src/mdbook_preprocessor.rs b/crates/mdbook-goals/src/mdbook_preprocessor.rs index 6da4494a..7b292651 100644 --- a/crates/mdbook-goals/src/mdbook_preprocessor.rs +++ b/crates/mdbook-goals/src/mdbook_preprocessor.rs @@ -16,6 +16,7 @@ use rust_project_goals::{ goal::{self, GoalDocument, Status, TeamAsk}, re, team, }; +use spanned::Spanned; const LINKS: &str = "links"; const LINKIFIERS: &str = "linkifiers"; @@ -293,18 +294,18 @@ impl<'c> GoalPreprocessorWithContext<'c> { } let config = Configuration::get(); let rows = std::iter::once(vec![ - "Ask".to_string(), - "aka".to_string(), - "Description".to_string(), + Spanned::here("Ask".to_string()), + Spanned::here("aka".to_string()), + Spanned::here("Description".to_string()), ]) .chain(config.team_asks.iter().map(|(name, details)| { vec![ - format!("{name:?}"), - details.short.to_string(), - details.about.to_string(), + Spanned::here(format!("{name:?}")), + Spanned::here(details.short.to_string()), + Spanned::here(details.about.to_string()), ] })) - .collect::>>(); + .collect::>>>(); let table = util::format_table(&rows); let new_content = re::VALID_TEAM_ASKS.replace_all(&chapter.content, table); chapter.content = new_content.to_string(); diff --git a/crates/rust-project-goals-cli/Cargo.toml b/crates/rust-project-goals-cli/Cargo.toml index 66ad46c5..92e3071e 100644 --- a/crates/rust-project-goals-cli/Cargo.toml +++ b/crates/rust-project-goals-cli/Cargo.toml @@ -16,3 +16,4 @@ clap = { version = "4.5.23", features = ["derive"] } rust-project-goals-json = { version = "0.1.0", path = "../rust-project-goals-json" } handlebars = { version = "6.2.0", features = ["dir_source"] } comrak = "0.31.0" +spanned = "0.4.0" diff --git a/crates/rust-project-goals-cli/src/rfc.rs b/crates/rust-project-goals-cli/src/rfc.rs index 2503304a..1894cc56 100644 --- a/crates/rust-project-goals-cli/src/rfc.rs +++ b/crates/rust-project-goals-cli/src/rfc.rs @@ -487,7 +487,7 @@ fn task_items(goal_plan: &GoalPlan) -> anyhow::Result> { let mut tasks = vec![]; if let Some(title) = &goal_plan.subgoal { - tasks.push(format!("### {title}")); + tasks.push(format!("### {}", **title)); } for plan_item in &goal_plan.plan_items { diff --git a/crates/rust-project-goals-cli/src/updates.rs b/crates/rust-project-goals-cli/src/updates.rs index 48e301fb..9be83489 100644 --- a/crates/rust-project-goals-cli/src/updates.rs +++ b/crates/rust-project-goals-cli/src/updates.rs @@ -5,6 +5,7 @@ use rust_project_goals::markwaydown; use rust_project_goals::re::{HELP_WANTED, TLDR}; use rust_project_goals::util::comma; use rust_project_goals_json::GithubIssueState; +use spanned::{Span, Spanned}; use std::io::Write; use std::path::Path; use std::process::{Command, Stdio}; @@ -229,7 +230,11 @@ fn help_wanted( } fn why_this_goal(issue_id: &IssueId, issue: &ExistingGithubIssue) -> anyhow::Result { - let sections = markwaydown::parse_text(issue_id.url(), &issue.body)?; + let span = Span { + file: issue_id.url().into(), + bytes: 0..0, + }; + let sections = markwaydown::parse_text(Spanned::new(&issue.body, span))?; for section in sections { if section.title == "Why this goal?" { return Ok(section.text.trim().to_string()); diff --git a/crates/rust-project-goals/Cargo.toml b/crates/rust-project-goals/Cargo.toml index 4ef51f56..a29578a4 100644 --- a/crates/rust-project-goals/Cargo.toml +++ b/crates/rust-project-goals/Cargo.toml @@ -16,3 +16,4 @@ rust_team_data = { git = "https://github.com/rust-lang/team" } rust-project-goals-json = { version = "0.1.0", path = "../rust-project-goals-json" } toml = "0.8.19" indexmap = "2.7.1" +spanned = "0.4.0" diff --git a/crates/rust-project-goals/src/format_team_ask.rs b/crates/rust-project-goals/src/format_team_ask.rs index 853b4847..f58072cd 100644 --- a/crates/rust-project-goals/src/format_team_ask.rs +++ b/crates/rust-project-goals/src/format_team_ask.rs @@ -3,6 +3,8 @@ use std::{ path::PathBuf, }; +use spanned::Spanned; + use crate::{ config::Configuration, goal::TeamAsk, @@ -117,19 +119,20 @@ pub fn format_team_asks(asks_of_any_team: &[&TeamAsk]) -> anyhow::Result // Create the table itself. let table = { - let headings = std::iter::once("Goal".to_string()) + let headings = std::iter::once(Spanned::here("Goal".to_string())) .chain(ask_headings.iter().map(|&ask_kind| { - format!( + Spanned::here(format!( "[{team_ask_short}][valid_team_asks]", // HACK: This should not be hardcoded in the code. team_ask_short = config.team_asks[ask_kind].short, - ) + )) })) // e.g. "discussion and moral support" - .collect::>(); + .collect::>>(); let rows = goal_rows.into_iter().map(|(goal_data, goal_columns)| { std::iter::once(goal_data.goal_title()) .chain(goal_columns) - .collect::>() + .map(Spanned::here) + .collect::>>() }); std::iter::once(headings).chain(rows).collect::>() diff --git a/crates/rust-project-goals/src/goal.rs b/crates/rust-project-goals/src/goal.rs index cf39bbdd..ab5787a3 100644 --- a/crates/rust-project-goals/src/goal.rs +++ b/crates/rust-project-goals/src/goal.rs @@ -4,6 +4,7 @@ use std::{collections::BTreeSet, path::PathBuf}; use anyhow::{bail, Context}; use regex::Regex; +use spanned::Spanned; use crate::config::{Configuration, TeamAskDetails}; use crate::gh::issue_id::{IssueId, Repository}; @@ -43,11 +44,11 @@ pub struct GoalDocument { pub struct Metadata { #[allow(unused)] pub title: String, - pub short_title: String, + pub short_title: Spanned, pub pocs: String, pub status: Status, pub tracking_issue: Option, - pub table: Table, + pub table: Spanned, } pub const TRACKING_ISSUE_ROW: &str = "Tracking issue"; @@ -56,7 +57,7 @@ pub const TRACKING_ISSUE_ROW: &str = "Tracking issue"; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct GoalPlan { /// If `Some`, title of the subsection in which these items were found. - pub subgoal: Option, + pub subgoal: Option>, /// List of items found in the table. pub plan_items: Vec, @@ -90,7 +91,7 @@ pub struct TeamAsk { pub ask_description: String, /// Title(s) of the goal. The first element is the title of the goal. The second, if present, is the subgoal. - pub goal_titles: Vec, + pub goal_titles: Vec>, /// Name(s) of the teams being asked to do the thing pub teams: Vec<&'static TeamName>, @@ -182,7 +183,9 @@ impl GoalDocument { /// Modify the goal document on disk to link to the given issue number in the metadata. pub fn link_issue(&self, number: IssueId) -> anyhow::Result<()> { let mut metadata_table = self.metadata.table.clone(); - metadata_table.add_key_value_row(TRACKING_ISSUE_ROW, &number); + metadata_table + .content + .add_key_value_row(TRACKING_ISSUE_ROW, &number); self.metadata .table .overwrite_in_path(&self.path, &metadata_table)?; @@ -209,9 +212,9 @@ pub fn format_goal_table(goals: &[&GoalDocument]) -> anyhow::Result { if !goals_are_proposed { table = vec![vec![ - "Goal".to_string(), - "Point of contact".to_string(), - "Progress".to_string(), + Spanned::here("Goal".to_string()), + Spanned::here("Point of contact".to_string()), + Spanned::here("Progress".to_string()), ]]; for goal in goals { @@ -235,16 +238,20 @@ pub fn format_goal_table(goals: &[&GoalDocument]) -> anyhow::Result { }; table.push(vec![ - format!("[{}]({})", goal.metadata.title, goal.link_path.display()), - goal.point_of_contact_for_goal_list(), - progress_bar, + Spanned::here(format!( + "[{}]({})", + goal.metadata.title, + goal.link_path.display() + )), + Spanned::here(goal.point_of_contact_for_goal_list()), + Spanned::here(progress_bar), ]); } } else { table = vec![vec![ - "Goal".to_string(), - "Point of contact".to_string(), - "Team".to_string(), + Spanned::here("Goal".to_string()), + Spanned::here("Point of contact".to_string()), + Spanned::here("Team".to_string()), ]]; for goal in goals { @@ -256,9 +263,13 @@ pub fn format_goal_table(goals: &[&GoalDocument]) -> anyhow::Result { .collect(); let teams: Vec<&TeamName> = teams.into_iter().collect(); table.push(vec![ - format!("[{}]({})", goal.metadata.title, goal.link_path.display()), - goal.point_of_contact_for_goal_list(), - commas(&teams), + Spanned::here(format!( + "[{}]({})", + goal.metadata.title, + goal.link_path.display() + )), + Spanned::here(goal.point_of_contact_for_goal_list()), + Spanned::here(commas(&teams)), ]); } } @@ -398,8 +409,8 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { if !re::is_just(&re::USERNAME, poc_row[1].trim()) { anyhow::bail!( - "point of contact must be a single github username (found {:?})", - poc_row[1] + "point of contact must be a single github username (found {})", + poc_row[1].render() ) } @@ -425,7 +436,7 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { // For the others, it's of course optional. if has_tracking_issue { - Some(r[1].parse()?) + Some(r[1].parse().transpose()?.content) } else { None } @@ -439,9 +450,9 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { Ok(Some(Metadata { title: title.to_string(), short_title: if let Some(row) = short_title_row { - row[1].to_string() + row[1].clone() } else { - title.to_string() + title.clone() }, pocs: poc_row[1].to_string(), status, @@ -450,7 +461,7 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { })) } -fn verify_row(rows: &[Vec], key: &str, value: &str) -> anyhow::Result<()> { +fn verify_row(rows: &[Vec>], key: &str, value: &str) -> anyhow::Result<()> { let Some(row) = rows.iter().find(|row| row[0] == key) else { anyhow::bail!("metadata table has no `{}` row", key) }; @@ -505,7 +516,10 @@ fn extract_plan_items<'i>(sections: &[Section]) -> anyhow::Result> Ok(goal_plans) } -fn goal_plan(subgoal: Option, section: &Section) -> anyhow::Result> { +fn goal_plan( + subgoal: Option>, + section: &Section, +) -> anyhow::Result> { match section.tables.len() { 0 => Ok(None), 1 => { @@ -523,12 +537,15 @@ fn goal_plan(subgoal: Option, section: &Section) -> anyhow::Result anyhow::bail!("multiple goal tables found in section `{}`", section.title), + _ => anyhow::bail!( + "multiple goal tables found in section `{}`", + section.title.render() + ), } } fn extract_plan_item( - rows: &mut std::iter::Peekable>>, + rows: &mut std::iter::Peekable>>>, ) -> anyhow::Result { let Some(row) = rows.next() else { anyhow::bail!("unexpected end of table"); @@ -620,7 +637,7 @@ impl PlanItem { fn team_asks( &self, link_path: &Arc, - goal_titles: &Vec, + goal_titles: &Vec>, goal_owners: &str, ) -> anyhow::Result> { let mut asks = vec![]; @@ -659,11 +676,12 @@ impl PlanItem { fn expect_headers(table: &Table, expected: &[&str]) -> anyhow::Result<()> { if table.header != expected { + // FIXME: do a diff so we see which headers are missing or extraneous anyhow::bail!( - "on line {}, unexpected table header, expected `{:?}`, found `{:?}`", - table.line_num, + "{}: unexpected table header, expected `{:?}`, found `{:?}`", + table.header[0].render(), expected, - table.header + table.header.iter().map(|h| &h.content), ); } diff --git a/crates/rust-project-goals/src/markwaydown.rs b/crates/rust-project-goals/src/markwaydown.rs index bec72c2f..718b6407 100644 --- a/crates/rust-project-goals/src/markwaydown.rs +++ b/crates/rust-project-goals/src/markwaydown.rs @@ -2,59 +2,56 @@ use std::{fmt::Display, path::Path}; +use spanned::Spanned; + use crate::util; /// A "section" is a piece of markdown that begins with `##` and which extends until the next section. /// Note that we don't track the hierarchical structure of sections in particular. #[derive(Debug)] pub struct Section { - /// Line numberin the document - pub line_num: usize, - /// Number of hashes pub level: usize, /// Title of the section -- what came after the `#` in the markdown. - pub title: String, + pub title: Spanned, /// Markdown text until start of next section, excluding tables - pub text: String, + pub text: Spanned, /// Tables are parsed and stored here - pub tables: Vec
, + pub tables: Vec>, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Table { - pub line_num: usize, - pub header: Vec, - pub rows: Vec>, + pub header: Vec>, + pub rows: Vec>>, } pub fn parse(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); - let text = std::fs::read_to_string(path)?; - parse_text(path, &text) + let text = Spanned::read_str_from_file(path).transpose()?; + parse_text(text.as_ref()) } -pub fn parse_text(path: impl AsRef, text: &str) -> anyhow::Result> { - let path: &Path = path.as_ref(); +pub fn parse_text(text: Spanned<&str>) -> anyhow::Result> { let mut result = vec![]; let mut open_section = None; let mut open_table = None; - for (line, line_num) in text.lines().zip(1..) { - let categorized = categorize_line(line); + for line in text.lines() { + let line = line.to_str().unwrap(); + let categorized = categorize_line(line.clone()); // eprintln!("line = {:?}", line); // eprintln!("categorized = {:?}", categorized); match categorized { CategorizeLine::Title(level, title) => { close_section(&mut result, &mut open_section, &mut open_table); open_section = Some(Section { - line_num, level, title, - text: String::new(), + text: Default::default(), tables: vec![], }); } @@ -62,10 +59,9 @@ pub fn parse_text(path: impl AsRef, text: &str) -> anyhow::Result, text: &str) -> anyhow::Result table.header.len() { return Err(anyhow::anyhow!( - "{}:{}: too many columns in table, expected no more than {}", - path.display(), - line_num, + "{}: too many columns in table, expected no more than {}", + row[table.header.len()].span, table.header.len() )); } while row.len() < table.header.len() { - row.push(String::new()); + row.push(Spanned::here(String::new())); } - table.rows.push(row); + table.content.rows.push(row); } else { - open_table = Some(Table { - line_num, - header: row, - rows: vec![], - }); + open_table = Some(Spanned::new( + Table { + header: row, + rows: vec![], + }, + line.span.clone().shrink_to_start(), + )); } } - CategorizeLine::TableDashRow(len) => { + CategorizeLine::TableDashRow(dashes) => { if let Some(table) = &open_table { - if table.header.len() != len { + if table.header.len() != dashes.len() { return Err(anyhow::anyhow!( - "{}:{}: too many columns in table, expected no more than {}", - path.display(), - line_num, + "{:?}: invalid number of columns in table, expected {}", + dashes.last().unwrap(), table.header.len() )); } - if !table.rows.is_empty() { + if let Some(first) = table.rows.first() { return Err(anyhow::anyhow!( - "{}:{}: did not expect table header here, already saw table rows", - path.display(), - line_num, + "{:?}: did not expect table header here, already saw table rows: {:?}", + dashes[0].span, + first[0].span, )); } } else { return Err(anyhow::anyhow!( - "{}:{}: did not expect table header here", - path.display(), - line_num, + "{:?}: did not expect table header here", + dashes[0] )); } } CategorizeLine::Other => { close_table(&mut open_section, &mut open_table); if let Some(section) = open_section.as_mut() { - section.text.push_str(line); - section.text.push('\n'); + section.text.span.bytes.end = line.span.bytes.end; + section.text.content.push_str(&**line); + section.text.content.push('\n'); } } } @@ -134,7 +130,7 @@ pub fn parse_text(path: impl AsRef, text: &str) -> anyhow::Result, open_table: &mut Option
) { +fn close_table(open_section: &mut Option
, open_table: &mut Option>) { if let Some(table) = open_table.take() { open_section.as_mut().unwrap().tables.push(table); } @@ -143,7 +139,7 @@ fn close_table(open_section: &mut Option
, open_table: &mut Option
, open_section: &mut Option
, - open_table: &mut Option
, + open_table: &mut Option>, ) { close_table(open_section, open_table); if let Some(section) = open_section.take() { @@ -153,23 +149,23 @@ fn close_section( #[derive(Debug)] enum CategorizeLine { - Title(usize, String), - TableRow(Vec), - TableDashRow(usize), + Title(usize, Spanned), + TableRow(Vec>), + TableDashRow(Vec>), Other, } -fn categorize_line(line: &str) -> CategorizeLine { - if line.starts_with('#') { - let level = line.chars().take_while(|&ch| ch == '#').count(); +fn categorize_line(line: Spanned<&str>) -> CategorizeLine { + if line.starts_with("#") { + let level = line.chars().take_while(|ch| **ch == '#').count(); CategorizeLine::Title(level, line.trim_start_matches('#').trim().to_string()) } else if let Some(line) = line - .strip_prefix('|') - .and_then(|line| line.strip_suffix('|')) + .strip_prefix("|") + .and_then(|line| line.strip_suffix("|")) { let columns = line.split('|').map(|s| s.trim()); - if columns.clone().all(|s| s.chars().all(|c| c == '-')) { - CategorizeLine::TableDashRow(columns.count()) + if columns.clone().all(|s| s.chars().all(|c| *c == '-')) { + CategorizeLine::TableDashRow(columns.map(|s| s.map(drop)).collect()) } else { CategorizeLine::TableRow(columns.map(|s| s.to_string()).collect()) } @@ -187,12 +183,15 @@ impl Table { match self.rows.iter_mut().find(|row| row[0] == row_key) { Some(row) => { - row[1] = row_value.to_string(); + // FIXME(oli-obk): get proper spans + row[1] = Spanned::here(row_value.to_string()); } None => { - self.rows - .push(vec![row_key.to_string(), row_value.to_string()]); + self.rows.push(vec![ + Spanned::here(row_key.to_string()), + Spanned::here(row_value.to_string()), + ]); } } } @@ -201,30 +200,16 @@ impl Table { pub fn overwrite_in_path(&self, path: &Path, new_table: &Table) -> anyhow::Result<()> { let full_text = std::fs::read_to_string(path)?; - let mut new_lines = vec![]; - new_lines.extend( - full_text - .lines() - .take(self.line_num - 1) - .map(|s| s.to_string()), - ); + let mut new_text = full_text[..self.header[0].span.bytes.start].to_string(); let table_text = { let mut new_rows = vec![new_table.header.clone()]; new_rows.extend(new_table.rows.iter().cloned()); util::format_table(&new_rows) }; - new_lines.push(table_text); - - new_lines.extend( - full_text - .lines() - .skip(self.line_num - 1) - .skip(2 + self.rows.len()) - .map(|s| s.to_string()), - ); + new_text.push_str(&table_text); + new_text.push_str(&full_text[self.rows.last().unwrap().last().unwrap().span.bytes.end..]); - let new_text = new_lines.join("\n"); std::fs::write(path, new_text)?; Ok(()) diff --git a/crates/rust-project-goals/src/util.rs b/crates/rust-project-goals/src/util.rs index dfb092f8..04481009 100644 --- a/crates/rust-project-goals/src/util.rs +++ b/crates/rust-project-goals/src/util.rs @@ -4,6 +4,7 @@ use std::{ path::{Path, PathBuf}, }; +use spanned::Spanned; use walkdir::WalkDir; pub const ARROW: &str = "↳"; @@ -11,7 +12,7 @@ pub const ARROW: &str = "↳"; /// Formats a table as markdown. The input should be a series of rows /// where each row has the same number of columns. /// The first row is the headers. -pub fn format_table(rows: &[Vec]) -> String { +pub fn format_table(rows: &[Vec>]) -> String { let mut output = String::new(); let Some((header_row, data_rows)) = rows.split_first() else { @@ -31,7 +32,13 @@ pub fn format_table(rows: &[Vec]) -> String { for (text, col) in columns.iter().zip(0..) { output.push('|'); - write!(output, " {text: