diff --git a/crates/treehouse/src/cli/fix.rs b/crates/treehouse/src/cli/fix.rs index 2da6251..c1eb6c3 100644 --- a/crates/treehouse/src/cli/fix.rs +++ b/crates/treehouse/src/cli/fix.rs @@ -108,7 +108,8 @@ pub fn fix_file( diagnostics: &mut Vec>, file_id: FileId, ) -> Result { - parse_tree_with_diagnostics(treehouse, file_id) + let source = treehouse.source(file_id).input(); + parse_tree_with_diagnostics(file_id, source) .map(|roots| { let mut source = treehouse.source(file_id).input().to_owned(); let mut state = State::default(); @@ -146,7 +147,7 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result { let mut treehouse = Treehouse::new(); let mut diagnostics = vec![]; - let file_id = treehouse.add_file(fix_args.file.as_str().to_owned(), Source::Other(file)); + let file_id = treehouse.add_file(fix_args.file.clone(), Source::Other(file)); let edit_path = root.edit_path(&fix_args.file).ok_or_else(|| { anyhow!( "{} is not an editable file (perhaps it is not in a persistent path?)", @@ -178,7 +179,7 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result { Edit::NoOp } } else { - report_diagnostics(&treehouse.files, &diagnostics)?; + report_diagnostics(&treehouse, &diagnostics)?; Edit::NoOp }, ) @@ -196,7 +197,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result anyhow::Result>) }); if let Some(branch_id) = branch_id { let branch = state.sources.treehouse.tree.branch(branch_id); - if let Source::Tree { - input, target_path, .. - } = state.sources.treehouse.source(branch.file_id) + if let Source::Tree { input, tree_path } = + state.sources.treehouse.source(branch.file_id) { if let Some(content) = state .target - .content(target_path) + .content(tree_path) .await .and_then(|s| String::from_utf8(s).ok()) { @@ -172,7 +171,7 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State>) } else { return ( StatusCode::INTERNAL_SERVER_ERROR, - format!("500 Internal Server Error: branch metadata points to entry {target_path} which does not have readable content") + format!("500 Internal Server Error: branch metadata points to entry {tree_path} which does not have readable content") ) .into_response(); } diff --git a/crates/treehouse/src/cli/wc.rs b/crates/treehouse/src/cli/wc.rs index 609727b..27ad3a6 100644 --- a/crates/treehouse/src/cli/wc.rs +++ b/crates/treehouse/src/cli/wc.rs @@ -47,16 +47,16 @@ pub fn wc_cli(content_dir: &dyn Dir, mut wc_args: WcArgs) -> anyhow::Result<()> .content(path) .and_then(|b| String::from_utf8(b).ok()) { - let file_id = treehouse.add_file(path.to_string(), Source::Other(content)); - match parse_tree_with_diagnostics(&mut treehouse, file_id) { + let file_id = treehouse.add_file(path.clone(), Source::Other(content.clone())); + match parse_tree_with_diagnostics(file_id, &content) { Ok(parsed) => { let source = treehouse.source(file_id); let word_count = wc_roots(source.input(), &parsed); - println!("{word_count:>8} {}", treehouse.filename(file_id)); + println!("{word_count:>8} {}", treehouse.path(file_id)); total += word_count; } Err(diagnostics) => { - report_diagnostics(&treehouse.files, &diagnostics)?; + report_diagnostics(&treehouse, &diagnostics)?; } } } diff --git a/crates/treehouse/src/generate.rs b/crates/treehouse/src/generate.rs index 2879e0a..0f87a2e 100644 --- a/crates/treehouse/src/generate.rs +++ b/crates/treehouse/src/generate.rs @@ -4,10 +4,10 @@ mod include_static_helper; use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc}; use anyhow::{anyhow, ensure, Context}; -use codespan_reporting::diagnostic::Diagnostic; use dir_helper::DirHelper; use handlebars::{handlebars_helper, Handlebars}; use include_static_helper::IncludeStaticHelper; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; use serde::Serialize; use tracing::{error, info_span, instrument}; @@ -18,7 +18,7 @@ use crate::{ html::{breadcrumbs::breadcrumbs_to_html, navmap::NavigationMap, tree::branches_to_html}, import_map::ImportMap, parse::parse_tree_with_diagnostics, - state::{report_diagnostics, Source}, + state::{report_diagnostics, FileId, Source}, tree::SemaRoots, vfs::{ self, Cd, ContentCache, Dir, DirEntry, DynDir, EditPath, ImageSize, MemDir, Overlay, @@ -26,13 +26,7 @@ use crate::{ }, }; -use crate::state::{FileId, Treehouse}; - -#[derive(Debug, Clone)] -pub struct ParsedTree { - root_key: String, - file_id: FileId, -} +use crate::state::Treehouse; #[derive(Serialize)] struct Page { @@ -96,84 +90,81 @@ fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) { }); } -#[instrument(skip(treehouse, config, source, target_path, tree_path))] -fn parse_tree( - treehouse: &mut Treehouse, - config: &Config, - source: String, - source_path: VPathBuf, - target_path: VPathBuf, - tree_path: String, -) -> anyhow::Result<(Option, Vec>)> { - let file_id = treehouse.add_file( - source_path.as_str().to_owned(), - Source::Tree { - input: source, - target_path: target_path.clone(), - tree_path: tree_path.clone(), - }, - ); - - match parse_tree_with_diagnostics(treehouse, file_id) { - Ok(roots) => { - let mut diagnostics = vec![]; - let roots = SemaRoots::from_roots(treehouse, &mut diagnostics, config, file_id, roots); - - let root_key = tree_path.clone(); - treehouse.roots.insert(root_key.clone(), roots); - - Ok((Some(ParsedTree { root_key, file_id }), diagnostics)) - } - Err(diagnostics) => Ok((None, diagnostics)), - } -} - #[instrument(skip(config, dirs))] -fn parse_trees( - config: &Config, - dirs: &Dirs, -) -> anyhow::Result<(Treehouse, HashMap)> { +fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result { let mut treehouse = Treehouse::new(); let mut diagnostics = vec![]; let mut parsed_trees = HashMap::new(); + let mut paths = vec![]; + vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| { if path.extension() == Some("tree") { - if let Some(source) = dirs - .content - .content(path) - .and_then(|b| String::from_utf8(b).ok()) - { - let tree_path = path.with_extension(""); - let target_path = path.with_extension("html"); - - match parse_tree( - &mut treehouse, - config, - source, - path.to_owned(), - target_path, - tree_path.as_str().to_owned(), - ) { - Ok((parsed_tree, mut parse_diagnostics)) => { - diagnostics.append(&mut parse_diagnostics); - if let Some(parsed_tree) = parsed_tree { - parsed_trees.insert(tree_path, parsed_tree); - } - } - Err(err) => { - error!("failed to parse tree {path}: {err:?}") - } - } - } + paths.push(path.to_owned()); } - ControlFlow::Continue(()) }); - report_diagnostics(&treehouse.files, &diagnostics)?; + // NOTE: Sources are filled in later; they can be left out until a call to report_diagnostics. + let file_ids: Vec<_> = paths + .iter() + .map(|path| treehouse.add_file(path.clone(), Source::Other(String::new()))) + .collect(); - Ok((treehouse, parsed_trees)) + let parse_results: Vec<_> = { + let _span = info_span!("load_trees::parse").entered(); + paths + .into_par_iter() + .zip(&file_ids) + .flat_map(|(path, &file_id)| { + dirs.content + .content(&path) + .and_then(|b| String::from_utf8(b).ok()) + .map(|input| { + let parse_result = parse_tree_with_diagnostics(file_id, &input); + (path, file_id, input, parse_result) + }) + }) + .collect() + }; + + for (path, file_id, input, _) in &parse_results { + let tree_path = path.with_extension(""); + treehouse + .files_by_tree_path + .insert(tree_path.clone(), *file_id); + treehouse.set_source( + *file_id, + Source::Tree { + input: input.clone(), + tree_path, + }, + ); + } + + { + let _span = info_span!("load_trees::sema").entered(); + for (path, file_id, _, result) in parse_results { + match result { + Ok(roots) => { + let roots = SemaRoots::from_roots( + &mut treehouse, + &mut diagnostics, + config, + file_id, + roots, + ); + treehouse.roots.insert(file_id, roots); + parsed_trees.insert(path, file_id); + } + Err(mut parse_diagnostics) => diagnostics.append(&mut parse_diagnostics), + } + } + } + + report_diagnostics(&treehouse, &diagnostics)?; + + Ok(treehouse) } #[instrument(skip(sources, handlebars))] @@ -205,23 +196,19 @@ fn generate_simple_template_or_error( } } -#[instrument(skip(sources, dirs, handlebars, parsed_tree), fields(root_key = parsed_tree.root_key))] +#[instrument(skip(sources, dirs, handlebars))] fn generate_tree( sources: &Sources, dirs: &Dirs, handlebars: &Handlebars, - parsed_tree: &ParsedTree, + file_id: FileId, ) -> anyhow::Result { - let breadcrumbs = breadcrumbs_to_html( - &sources.config, - &sources.navigation_map, - &parsed_tree.root_key, - ); + let breadcrumbs = breadcrumbs_to_html(&sources.config, &sources.navigation_map, file_id); let roots = sources .treehouse .roots - .get(&parsed_tree.root_key) + .get(&file_id) .expect("tree should have been added to the treehouse"); let tree = { @@ -232,7 +219,7 @@ fn generate_tree( &sources.treehouse, &sources.config, dirs, - parsed_tree.file_id, + file_id, &roots.branches, ); tree @@ -261,10 +248,7 @@ fn generate_tree( scripts: roots.attributes.scripts.clone(), styles: roots.attributes.styles.clone(), breadcrumbs, - tree_path: sources - .treehouse - .tree_path(parsed_tree.file_id) - .map(|s| s.to_owned()), + tree_path: sources.treehouse.tree_path(file_id).map(|s| s.to_string()), tree, }, }; @@ -289,9 +273,9 @@ fn generate_tree_or_error( sources: &Sources, dirs: &Dirs, handlebars: &Handlebars, - parsed_tree: &ParsedTree, + file_id: FileId, ) -> String { - match generate_tree(sources, dirs, handlebars, parsed_tree) { + match generate_tree(sources, dirs, handlebars, file_id) { Ok(html) => html, Err(error) => format!("error: {error:?}"), } @@ -300,7 +284,6 @@ fn generate_tree_or_error( pub struct Sources { pub config: Config, pub treehouse: Treehouse, - pub parsed_trees: HashMap, pub navigation_map: NavigationMap, pub import_map: ImportMap, } @@ -324,8 +307,11 @@ impl Sources { config }; - let (treehouse, parsed_trees) = parse_trees(&config, dirs)?; - let navigation_map = NavigationMap::build(&treehouse, "index"); + let treehouse = load_trees(&config, dirs)?; + let navigation_map = NavigationMap::build( + &treehouse, + treehouse.files_by_tree_path[VPath::new("index")], + ); let import_map = ImportMap::generate( &config.site, &Cd::new(dirs.static_.clone(), VPathBuf::new("js")), @@ -335,7 +321,6 @@ impl Sources { Ok(Sources { config, treehouse, - parsed_trees, navigation_map, import_map, }) @@ -429,11 +414,11 @@ impl Dir for TreehouseDir { }; self.sources - .parsed_trees + .treehouse + .files_by_tree_path .get(path) - .map(|parsed_tree| { - generate_tree_or_error(&self.sources, &self.dirs, &self.handlebars, parsed_tree) - .into() + .map(|&file_id| { + generate_tree_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into() }) .or_else(|| { if path.file_name().is_some_and(|s| !s.starts_with('_')) { @@ -525,7 +510,7 @@ pub fn target(dirs: Arc, sources: Arc) -> DynDir { Cd::new(dirs.static_.clone(), VPathBuf::new("robots.txt")).to_dyn(), ); - let dir_index = DirIndex::new(sources.parsed_trees.keys().map(|x| &**x)); + let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x)); let tree_view = TreehouseDir::new(dirs, sources, dir_index); let tree_view = ContentCache::new(tree_view); diff --git a/crates/treehouse/src/html/breadcrumbs.rs b/crates/treehouse/src/html/breadcrumbs.rs index b78a375..d71555c 100644 --- a/crates/treehouse/src/html/breadcrumbs.rs +++ b/crates/treehouse/src/html/breadcrumbs.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, fmt::Write}; use tracing::instrument; -use crate::config::Config; +use crate::{config::Config, state::FileId, vfs::VPath}; use super::{navmap::NavigationMap, EscapeAttribute}; @@ -10,26 +10,31 @@ use super::{navmap::NavigationMap, EscapeAttribute}; pub fn breadcrumbs_to_html( config: &Config, navigation_map: &NavigationMap, - tree_path: &str, + file_id: FileId, ) -> String { let mut s = String::new(); - if let Some(path) = navigation_map.paths.get(tree_path) { + if let Some(path) = navigation_map.paths.get(&file_id) { for (i, element) in path.iter().enumerate() { // Skip the index because it's implied by the logo on the left. - if element != "index" { + if &**element != VPath::new_const("index") { s.push_str("
  • "); { let short_element = path .get(i - 1) .map(|p| format!("{p}/")) - .and_then(|prefix| element.strip_prefix(prefix.as_str()).map(Cow::Borrowed)) + .and_then(|prefix| { + element + .as_str() + .strip_prefix(prefix.as_str()) + .map(Cow::Borrowed) + }) .unwrap_or_else(|| Cow::Owned(format!("/{element}"))); write!( s, "{short_element}", site = EscapeAttribute(&config.site), - element = EscapeAttribute(element) + element = EscapeAttribute(element.as_str()) ) .unwrap(); } diff --git a/crates/treehouse/src/html/navmap.rs b/crates/treehouse/src/html/navmap.rs index d331b38..b4c9dd2 100644 --- a/crates/treehouse/src/html/navmap.rs +++ b/crates/treehouse/src/html/navmap.rs @@ -3,20 +3,23 @@ use std::collections::HashMap; use tracing::instrument; use crate::{ - state::Treehouse, + state::{FileId, Treehouse}, tree::{attributes::Content, SemaBranchId}, + vfs::VPathBuf, }; #[derive(Debug, Clone, Default)] struct NavigationMapBuilder { - stack: Vec, + stack: Vec, navigation_map: NavigationMap, } impl NavigationMapBuilder { - fn enter_tree(&mut self, tree: String) { - self.stack.push(tree.clone()); - self.navigation_map.paths.insert(tree, self.stack.clone()); + fn enter_tree(&mut self, file_id: FileId, tree_path: VPathBuf) { + self.stack.push(tree_path.clone()); + self.navigation_map + .paths + .insert(file_id, self.stack.clone()); } fn exit_tree(&mut self) { @@ -31,12 +34,12 @@ impl NavigationMapBuilder { #[derive(Debug, Clone, Default)] pub struct NavigationMap { /// Tells you which pages need to be opened to get to the key. - pub paths: HashMap>, + pub paths: HashMap>, } impl NavigationMap { #[instrument(name = "NavigationMap::build", skip(treehouse))] - pub fn build(treehouse: &Treehouse, root_tree_path: &str) -> Self { + pub fn build(treehouse: &Treehouse, root_file_id: FileId) -> Self { let mut builder = NavigationMapBuilder::default(); fn rec_branch( @@ -45,8 +48,8 @@ impl NavigationMap { branch_id: SemaBranchId, ) { let branch = treehouse.tree.branch(branch_id); - if let Content::Link(linked) = &branch.attributes.content { - rec_tree(treehouse, builder, linked); + if let Content::ResolvedLink(linked) = &branch.attributes.content { + rec_tree(treehouse, builder, *linked); } else { for &child_id in &branch.children { rec_branch(treehouse, builder, child_id); @@ -54,12 +57,18 @@ impl NavigationMap { } } - fn rec_tree(treehouse: &Treehouse, builder: &mut NavigationMapBuilder, tree_path: &str) { - if let Some(roots) = treehouse.roots.get(tree_path) { + fn rec_tree(treehouse: &Treehouse, builder: &mut NavigationMapBuilder, file_id: FileId) { + if let Some(roots) = treehouse.roots.get(&file_id) { // Pages can link to each other causing infinite recursion, so we need to handle that // case by skipping pages that already have been analyzed. - if !builder.navigation_map.paths.contains_key(tree_path) { - builder.enter_tree(tree_path.to_owned()); + if !builder.navigation_map.paths.contains_key(&file_id) { + builder.enter_tree( + file_id, + treehouse + .tree_path(file_id) + .expect("tree files may only link to other tree files") + .to_owned(), + ); for &branch_id in &roots.branches { rec_branch(treehouse, builder, branch_id); } @@ -68,7 +77,7 @@ impl NavigationMap { } } - rec_tree(treehouse, &mut builder, root_tree_path); + rec_tree(treehouse, &mut builder, root_file_id); builder.finish() } diff --git a/crates/treehouse/src/html/tree.rs b/crates/treehouse/src/html/tree.rs index 4cecc1b..816c1e2 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/crates/treehouse/src/html/tree.rs @@ -30,8 +30,8 @@ pub fn branch_to_html( return; } - let has_children = - !branch.children.is_empty() || matches!(branch.attributes.content, Content::Link(_)); + let has_children = !branch.children.is_empty() + || matches!(branch.attributes.content, Content::ResolvedLink(_)); let class = if has_children { "branch" } else { "leaf" }; let mut class = String::from(class); @@ -44,7 +44,7 @@ pub fn branch_to_html( class.push_str(" draft"); } - let component = if let Content::Link(_) = branch.attributes.content { + let component = if let Content::ResolvedLink(_) = branch.attributes.content { "b-linked" } else { "b" @@ -55,8 +55,9 @@ pub fn branch_to_html( Cow::Borrowed(component) }; - let linked_branch = if let Content::Link(link) = &branch.attributes.content { - format!(" data-th-link=\"{}\"", EscapeHtml(link)) + let linked_branch = if let Content::ResolvedLink(file_id) = &branch.attributes.content { + let path = treehouse.tree_path(*file_id).expect(".tree file expected"); + format!(" data-th-link=\"{}\"", EscapeHtml(path.as_str())) } else { String::new() }; @@ -126,7 +127,7 @@ pub fn branch_to_html( page_id: treehouse .tree_path(file_id) .expect(".tree file expected") - .to_owned(), + .to_string(), config, dirs, @@ -137,13 +138,14 @@ pub fn branch_to_html( .render(&events, s); let branch = treehouse.tree.branch(branch_id); - if let Content::Link(link) = &branch.attributes.content { + if let Content::ResolvedLink(file_id) = &branch.attributes.content { + let path = treehouse.tree_path(*file_id).expect(".tree file expected"); write!( s, "", EscapeAttribute(&config.site), - EscapeAttribute(link), - EscapeHtml(link), + EscapeAttribute(path.as_str()), + EscapeHtml(path.as_str()), ) .unwrap(); } @@ -151,12 +153,13 @@ pub fn branch_to_html( s.push_str(""); { - if let Content::Link(link) = &branch.attributes.content { + if let Content::ResolvedLink(file_id) = &branch.attributes.content { + let path = treehouse.tree_path(*file_id).expect(".tree file expected"); write!( s, "", EscapeAttribute(&config.site), - EscapeAttribute(link), + EscapeAttribute(path.as_str()), ) .unwrap(); } else { diff --git a/crates/treehouse/src/parse.rs b/crates/treehouse/src/parse.rs index 8193d81..56fe2f4 100644 --- a/crates/treehouse/src/parse.rs +++ b/crates/treehouse/src/parse.rs @@ -8,12 +8,11 @@ use crate::state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}; pub struct ErrorsEmitted; -#[instrument(skip(treehouse))] +#[instrument(skip(input))] pub fn parse_tree_with_diagnostics( - treehouse: &mut Treehouse, file_id: FileId, + input: &str, ) -> Result>> { - let input = &treehouse.source(file_id).input(); Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| { vec![Diagnostic { severity: Severity::Error, diff --git a/crates/treehouse/src/state.rs b/crates/treehouse/src/state.rs index eb2f80a..9ce0448 100644 --- a/crates/treehouse/src/state.rs +++ b/crates/treehouse/src/state.rs @@ -3,23 +3,19 @@ use std::{collections::HashMap, ops::Range}; use anyhow::Context; use codespan_reporting::{ diagnostic::{Diagnostic, Label, LabelStyle, Severity}, - files::SimpleFiles, term::termcolor::{ColorChoice, StandardStream}, }; +use tracing::instrument; use ulid::Ulid; use crate::{ tree::{SemaBranchId, SemaRoots, SemaTree}, - vfs::VPathBuf, + vfs::{VPath, VPathBuf}, }; #[derive(Debug, Clone)] pub enum Source { - Tree { - input: String, - tree_path: String, - target_path: VPathBuf, - }, + Tree { input: String, tree_path: VPathBuf }, Other(String), } @@ -38,26 +34,54 @@ impl AsRef for Source { } } -pub type Files = SimpleFiles; -pub type FileId = >::FileId; +#[derive(Debug, Clone)] +pub struct File { + pub path: VPathBuf, + pub source: Source, + pub line_starts: Vec, +} + +impl File { + fn line_start(&self, line_index: usize) -> Result { + use std::cmp::Ordering; + + match line_index.cmp(&self.line_starts.len()) { + Ordering::Less => Ok(self + .line_starts + .get(line_index) + .cloned() + .expect("failed despite previous check")), + Ordering::Equal => Ok(self.source.as_ref().len()), + Ordering::Greater => Err(codespan_reporting::files::Error::LineTooLarge { + given: line_index, + max: self.line_starts.len() - 1, + }), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FileId(usize); /// Treehouse compilation context. pub struct Treehouse { - pub files: Files, + pub files: Vec, + pub files_by_tree_path: HashMap, pub tree: SemaTree, pub branches_by_named_id: HashMap, - pub roots: HashMap, + pub roots: HashMap, pub branch_redirects: HashMap, - missingno_generator: ulid::Generator, + pub missingno_generator: ulid::Generator, } impl Treehouse { pub fn new() -> Self { Self { - files: Files::new(), + files: vec![], + files_by_tree_path: HashMap::new(), tree: SemaTree::default(), branches_by_named_id: HashMap::new(), @@ -69,27 +93,34 @@ impl Treehouse { } } - pub fn add_file(&mut self, filename: String, source: Source) -> FileId { - self.files.add(filename, source) + pub fn add_file(&mut self, path: VPathBuf, source: Source) -> FileId { + let id = FileId(self.files.len()); + self.files.push(File { + line_starts: codespan_reporting::files::line_starts(source.input()).collect(), + + path, + source, + }); + id + } + + /// Get the name of a file, assuming it was previously registered. + pub fn path(&self, file_id: FileId) -> &VPath { + &self.files[file_id.0].path } /// Get the source code of a file, assuming it was previously registered. pub fn source(&self, file_id: FileId) -> &Source { - self.files - .get(file_id) - .expect("file should have been registered previously") - .source() + &self.files[file_id.0].source } - /// Get the name of a file, assuming it was previously registered. - pub fn filename(&self, file_id: FileId) -> &str { - self.files - .get(file_id) - .expect("file should have been registered previously") - .name() + pub fn set_source(&mut self, file_id: FileId, source: Source) { + self.files[file_id.0].line_starts = + codespan_reporting::files::line_starts(source.input()).collect(); + self.files[file_id.0].source = source; } - pub fn tree_path(&self, file_id: FileId) -> Option<&str> { + pub fn tree_path(&self, file_id: FileId) -> Option<&VPath> { match self.source(file_id) { Source::Tree { tree_path, .. } => Some(tree_path), Source::Other(_) => None, @@ -109,6 +140,49 @@ impl Default for Treehouse { } } +impl<'a> codespan_reporting::files::Files<'a> for Treehouse { + type FileId = FileId; + + type Name = &'a VPath; + + type Source = &'a str; + + fn name(&'a self, id: Self::FileId) -> Result { + Ok(self.path(id)) + } + + fn source( + &'a self, + id: Self::FileId, + ) -> Result { + Ok(self.source(id).input()) + } + + fn line_index( + &'a self, + id: Self::FileId, + byte_index: usize, + ) -> Result { + let file = &self.files[id.0]; + Ok(file + .line_starts + .binary_search(&byte_index) + .unwrap_or_else(|next_line| next_line - 1)) + } + + fn line_range( + &'a self, + id: Self::FileId, + line_index: usize, + ) -> Result, codespan_reporting::files::Error> { + let file = &self.files[id.0]; + let line_start = file.line_start(line_index)?; + let next_line_start = file.line_start(line_index + 1)?; + + Ok(line_start..next_line_start) + } +} + pub struct TomlError { pub message: String, pub span: Option>, @@ -135,7 +209,11 @@ pub fn toml_error_to_diagnostic(error: TomlError) -> Diagnostic { } } -pub fn report_diagnostics(files: &Files, diagnostics: &[Diagnostic]) -> anyhow::Result<()> { +#[instrument(skip(files, diagnostics))] +pub fn report_diagnostics( + files: &Treehouse, + diagnostics: &[Diagnostic], +) -> anyhow::Result<()> { let writer = StandardStream::stderr(ColorChoice::Auto); let config = codespan_reporting::term::Config::default(); for diagnostic in diagnostics { diff --git a/crates/treehouse/src/tree.rs b/crates/treehouse/src/tree.rs index ca72f57..ac0890e 100644 --- a/crates/treehouse/src/tree.rs +++ b/crates/treehouse/src/tree.rs @@ -96,7 +96,7 @@ impl SemaRoots { if successfully_parsed && attributes.title.is_empty() { attributes.title = match treehouse.source(file_id) { - Source::Tree { tree_path, .. } => tree_path.clone(), + Source::Tree { tree_path, .. } => tree_path.to_string(), _ => panic!("parse_attributes called for a non-.tree file"), } } @@ -310,7 +310,7 @@ impl SemaBranch { "note: a generated id `{}` will be used, but this id is unstable and will not persist across generations", attributes.id ), - format!("help: run `treehouse fix {}` to add missing ids to branches", treehouse.filename(file_id)), + format!("help: run `treehouse fix {}` to add missing ids to branches", treehouse.path(file_id)), ], }); } @@ -334,6 +334,26 @@ impl SemaBranch { }); } } + + // Resolve content.links. + if let Content::Link(tree_path) = &attributes.content { + if let Some(file_id) = treehouse.files_by_tree_path.get(tree_path) { + attributes.content = Content::ResolvedLink(*file_id); + } else { + diagnostics.push(Diagnostic { + severity: Severity::Error, + code: Some("attr".into()), + message: format!("linked tree `{tree_path}` does not exist"), + labels: vec![Label { + style: LabelStyle::Primary, + file_id, + range: attribute_warning_span.clone(), + message: "".into(), + }], + notes: vec![], + }) + } + } } attributes } diff --git a/crates/treehouse/src/tree/attributes.rs b/crates/treehouse/src/tree/attributes.rs index 8fbf2f9..ec7f4a7 100644 --- a/crates/treehouse/src/tree/attributes.rs +++ b/crates/treehouse/src/tree/attributes.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use crate::{state::FileId, vfs::VPathBuf}; + /// Top-level `%%` root attributes. #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct RootAttributes { @@ -118,7 +120,12 @@ pub enum Content { /// /// Note that `Link` branches must not contain any children. If a `Link` branch does contain /// children, an `attribute`-type error is raised. - Link(String), + Link(VPathBuf), + + /// Valid link to another tree. + /// This replaces `Content::Link` during semantic analysis. + #[serde(skip)] + ResolvedLink(FileId), } #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]