refactoring: remove dependency on SimpleFiles, make tree parsing multithreaded

This commit is contained in:
liquidex 2024-11-26 22:58:02 +01:00
parent 505163383f
commit 0713b59063
11 changed files with 283 additions and 177 deletions

View file

@ -108,7 +108,8 @@ pub fn fix_file(
diagnostics: &mut Vec<Diagnostic<FileId>>, diagnostics: &mut Vec<Diagnostic<FileId>>,
file_id: FileId, file_id: FileId,
) -> Result<String, parse::ErrorsEmitted> { ) -> Result<String, parse::ErrorsEmitted> {
parse_tree_with_diagnostics(treehouse, file_id) let source = treehouse.source(file_id).input();
parse_tree_with_diagnostics(file_id, source)
.map(|roots| { .map(|roots| {
let mut source = treehouse.source(file_id).input().to_owned(); let mut source = treehouse.source(file_id).input().to_owned();
let mut state = State::default(); let mut state = State::default();
@ -146,7 +147,7 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let mut diagnostics = vec![]; 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(|| { let edit_path = root.edit_path(&fix_args.file).ok_or_else(|| {
anyhow!( anyhow!(
"{} is not an editable file (perhaps it is not in a persistent path?)", "{} 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> {
Edit::NoOp Edit::NoOp
} }
} else { } else {
report_diagnostics(&treehouse.files, &diagnostics)?; report_diagnostics(&treehouse, &diagnostics)?;
Edit::NoOp Edit::NoOp
}, },
) )
@ -196,7 +197,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let mut diagnostics = vec![]; let mut diagnostics = vec![];
let file_id = treehouse.add_file(path.as_str().to_string(), Source::Other(content)); let file_id = treehouse.add_file(path.to_owned(), Source::Other(content));
let edit_path = dir.edit_path(path).context("path is not editable")?; let edit_path = dir.edit_path(path).context("path is not editable")?;
if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) { if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
@ -204,7 +205,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
return Ok(Edit::Write(edit_path, fixed)); return Ok(Edit::Write(edit_path, fixed));
} }
} else { } else {
report_diagnostics(&treehouse.files, &diagnostics)?; report_diagnostics(&treehouse, &diagnostics)?;
} }
} }

View file

@ -144,13 +144,12 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>)
}); });
if let Some(branch_id) = branch_id { if let Some(branch_id) = branch_id {
let branch = state.sources.treehouse.tree.branch(branch_id); let branch = state.sources.treehouse.tree.branch(branch_id);
if let Source::Tree { if let Source::Tree { input, tree_path } =
input, target_path, .. state.sources.treehouse.source(branch.file_id)
} = state.sources.treehouse.source(branch.file_id)
{ {
if let Some(content) = state if let Some(content) = state
.target .target
.content(target_path) .content(tree_path)
.await .await
.and_then(|s| String::from_utf8(s).ok()) .and_then(|s| String::from_utf8(s).ok())
{ {
@ -172,7 +171,7 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>)
} else { } else {
return ( return (
StatusCode::INTERNAL_SERVER_ERROR, 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(); .into_response();
} }

View file

@ -47,16 +47,16 @@ pub fn wc_cli(content_dir: &dyn Dir, mut wc_args: WcArgs) -> anyhow::Result<()>
.content(path) .content(path)
.and_then(|b| String::from_utf8(b).ok()) .and_then(|b| String::from_utf8(b).ok())
{ {
let file_id = treehouse.add_file(path.to_string(), Source::Other(content)); let file_id = treehouse.add_file(path.clone(), Source::Other(content.clone()));
match parse_tree_with_diagnostics(&mut treehouse, file_id) { match parse_tree_with_diagnostics(file_id, &content) {
Ok(parsed) => { Ok(parsed) => {
let source = treehouse.source(file_id); let source = treehouse.source(file_id);
let word_count = wc_roots(source.input(), &parsed); 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; total += word_count;
} }
Err(diagnostics) => { Err(diagnostics) => {
report_diagnostics(&treehouse.files, &diagnostics)?; report_diagnostics(&treehouse, &diagnostics)?;
} }
} }
} }

View file

@ -4,10 +4,10 @@ mod include_static_helper;
use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc}; use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc};
use anyhow::{anyhow, ensure, Context}; use anyhow::{anyhow, ensure, Context};
use codespan_reporting::diagnostic::Diagnostic;
use dir_helper::DirHelper; use dir_helper::DirHelper;
use handlebars::{handlebars_helper, Handlebars}; use handlebars::{handlebars_helper, Handlebars};
use include_static_helper::IncludeStaticHelper; use include_static_helper::IncludeStaticHelper;
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use serde::Serialize; use serde::Serialize;
use tracing::{error, info_span, instrument}; use tracing::{error, info_span, instrument};
@ -18,7 +18,7 @@ use crate::{
html::{breadcrumbs::breadcrumbs_to_html, navmap::NavigationMap, tree::branches_to_html}, html::{breadcrumbs::breadcrumbs_to_html, navmap::NavigationMap, tree::branches_to_html},
import_map::ImportMap, import_map::ImportMap,
parse::parse_tree_with_diagnostics, parse::parse_tree_with_diagnostics,
state::{report_diagnostics, Source}, state::{report_diagnostics, FileId, Source},
tree::SemaRoots, tree::SemaRoots,
vfs::{ vfs::{
self, Cd, ContentCache, Dir, DirEntry, DynDir, EditPath, ImageSize, MemDir, Overlay, self, Cd, ContentCache, Dir, DirEntry, DynDir, EditPath, ImageSize, MemDir, Overlay,
@ -26,13 +26,7 @@ use crate::{
}, },
}; };
use crate::state::{FileId, Treehouse}; use crate::state::Treehouse;
#[derive(Debug, Clone)]
pub struct ParsedTree {
root_key: String,
file_id: FileId,
}
#[derive(Serialize)] #[derive(Serialize)]
struct Page { 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<ParsedTree>, Vec<Diagnostic<FileId>>)> {
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))] #[instrument(skip(config, dirs))]
fn parse_trees( fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
config: &Config,
dirs: &Dirs,
) -> anyhow::Result<(Treehouse, HashMap<VPathBuf, ParsedTree>)> {
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let mut diagnostics = vec![]; let mut diagnostics = vec![];
let mut parsed_trees = HashMap::new(); let mut parsed_trees = HashMap::new();
let mut paths = vec![];
vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| { vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| {
if path.extension() == Some("tree") { if path.extension() == Some("tree") {
if let Some(source) = dirs paths.push(path.to_owned());
.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:?}")
}
}
}
}
ControlFlow::Continue(()) 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))] #[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( fn generate_tree(
sources: &Sources, sources: &Sources,
dirs: &Dirs, dirs: &Dirs,
handlebars: &Handlebars, handlebars: &Handlebars,
parsed_tree: &ParsedTree, file_id: FileId,
) -> anyhow::Result<String> { ) -> anyhow::Result<String> {
let breadcrumbs = breadcrumbs_to_html( let breadcrumbs = breadcrumbs_to_html(&sources.config, &sources.navigation_map, file_id);
&sources.config,
&sources.navigation_map,
&parsed_tree.root_key,
);
let roots = sources let roots = sources
.treehouse .treehouse
.roots .roots
.get(&parsed_tree.root_key) .get(&file_id)
.expect("tree should have been added to the treehouse"); .expect("tree should have been added to the treehouse");
let tree = { let tree = {
@ -232,7 +219,7 @@ fn generate_tree(
&sources.treehouse, &sources.treehouse,
&sources.config, &sources.config,
dirs, dirs,
parsed_tree.file_id, file_id,
&roots.branches, &roots.branches,
); );
tree tree
@ -261,10 +248,7 @@ fn generate_tree(
scripts: roots.attributes.scripts.clone(), scripts: roots.attributes.scripts.clone(),
styles: roots.attributes.styles.clone(), styles: roots.attributes.styles.clone(),
breadcrumbs, breadcrumbs,
tree_path: sources tree_path: sources.treehouse.tree_path(file_id).map(|s| s.to_string()),
.treehouse
.tree_path(parsed_tree.file_id)
.map(|s| s.to_owned()),
tree, tree,
}, },
}; };
@ -289,9 +273,9 @@ fn generate_tree_or_error(
sources: &Sources, sources: &Sources,
dirs: &Dirs, dirs: &Dirs,
handlebars: &Handlebars, handlebars: &Handlebars,
parsed_tree: &ParsedTree, file_id: FileId,
) -> String { ) -> String {
match generate_tree(sources, dirs, handlebars, parsed_tree) { match generate_tree(sources, dirs, handlebars, file_id) {
Ok(html) => html, Ok(html) => html,
Err(error) => format!("error: {error:?}"), Err(error) => format!("error: {error:?}"),
} }
@ -300,7 +284,6 @@ fn generate_tree_or_error(
pub struct Sources { pub struct Sources {
pub config: Config, pub config: Config,
pub treehouse: Treehouse, pub treehouse: Treehouse,
pub parsed_trees: HashMap<VPathBuf, ParsedTree>,
pub navigation_map: NavigationMap, pub navigation_map: NavigationMap,
pub import_map: ImportMap, pub import_map: ImportMap,
} }
@ -324,8 +307,11 @@ impl Sources {
config config
}; };
let (treehouse, parsed_trees) = parse_trees(&config, dirs)?; let treehouse = load_trees(&config, dirs)?;
let navigation_map = NavigationMap::build(&treehouse, "index"); let navigation_map = NavigationMap::build(
&treehouse,
treehouse.files_by_tree_path[VPath::new("index")],
);
let import_map = ImportMap::generate( let import_map = ImportMap::generate(
&config.site, &config.site,
&Cd::new(dirs.static_.clone(), VPathBuf::new("js")), &Cd::new(dirs.static_.clone(), VPathBuf::new("js")),
@ -335,7 +321,6 @@ impl Sources {
Ok(Sources { Ok(Sources {
config, config,
treehouse, treehouse,
parsed_trees,
navigation_map, navigation_map,
import_map, import_map,
}) })
@ -429,11 +414,11 @@ impl Dir for TreehouseDir {
}; };
self.sources self.sources
.parsed_trees .treehouse
.files_by_tree_path
.get(path) .get(path)
.map(|parsed_tree| { .map(|&file_id| {
generate_tree_or_error(&self.sources, &self.dirs, &self.handlebars, parsed_tree) generate_tree_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into()
.into()
}) })
.or_else(|| { .or_else(|| {
if path.file_name().is_some_and(|s| !s.starts_with('_')) { if path.file_name().is_some_and(|s| !s.starts_with('_')) {
@ -525,7 +510,7 @@ pub fn target(dirs: Arc<Dirs>, sources: Arc<Sources>) -> DynDir {
Cd::new(dirs.static_.clone(), VPathBuf::new("robots.txt")).to_dyn(), 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 = TreehouseDir::new(dirs, sources, dir_index);
let tree_view = ContentCache::new(tree_view); let tree_view = ContentCache::new(tree_view);

View file

@ -2,7 +2,7 @@ use std::{borrow::Cow, fmt::Write};
use tracing::instrument; use tracing::instrument;
use crate::config::Config; use crate::{config::Config, state::FileId, vfs::VPath};
use super::{navmap::NavigationMap, EscapeAttribute}; use super::{navmap::NavigationMap, EscapeAttribute};
@ -10,26 +10,31 @@ use super::{navmap::NavigationMap, EscapeAttribute};
pub fn breadcrumbs_to_html( pub fn breadcrumbs_to_html(
config: &Config, config: &Config,
navigation_map: &NavigationMap, navigation_map: &NavigationMap,
tree_path: &str, file_id: FileId,
) -> String { ) -> String {
let mut s = String::new(); 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() { for (i, element) in path.iter().enumerate() {
// Skip the index because it's implied by the logo on the left. // 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("<li class=\"breadcrumb\">"); s.push_str("<li class=\"breadcrumb\">");
{ {
let short_element = path let short_element = path
.get(i - 1) .get(i - 1)
.map(|p| format!("{p}/")) .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}"))); .unwrap_or_else(|| Cow::Owned(format!("/{element}")));
write!( write!(
s, s,
"<a href=\"{site}/{element}\">{short_element}</a>", "<a href=\"{site}/{element}\">{short_element}</a>",
site = EscapeAttribute(&config.site), site = EscapeAttribute(&config.site),
element = EscapeAttribute(element) element = EscapeAttribute(element.as_str())
) )
.unwrap(); .unwrap();
} }

View file

@ -3,20 +3,23 @@ use std::collections::HashMap;
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{
state::Treehouse, state::{FileId, Treehouse},
tree::{attributes::Content, SemaBranchId}, tree::{attributes::Content, SemaBranchId},
vfs::VPathBuf,
}; };
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct NavigationMapBuilder { struct NavigationMapBuilder {
stack: Vec<String>, stack: Vec<VPathBuf>,
navigation_map: NavigationMap, navigation_map: NavigationMap,
} }
impl NavigationMapBuilder { impl NavigationMapBuilder {
fn enter_tree(&mut self, tree: String) { fn enter_tree(&mut self, file_id: FileId, tree_path: VPathBuf) {
self.stack.push(tree.clone()); self.stack.push(tree_path.clone());
self.navigation_map.paths.insert(tree, self.stack.clone()); self.navigation_map
.paths
.insert(file_id, self.stack.clone());
} }
fn exit_tree(&mut self) { fn exit_tree(&mut self) {
@ -31,12 +34,12 @@ impl NavigationMapBuilder {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct NavigationMap { pub struct NavigationMap {
/// Tells you which pages need to be opened to get to the key. /// Tells you which pages need to be opened to get to the key.
pub paths: HashMap<String, Vec<String>>, pub paths: HashMap<FileId, Vec<VPathBuf>>,
} }
impl NavigationMap { impl NavigationMap {
#[instrument(name = "NavigationMap::build", skip(treehouse))] #[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(); let mut builder = NavigationMapBuilder::default();
fn rec_branch( fn rec_branch(
@ -45,8 +48,8 @@ impl NavigationMap {
branch_id: SemaBranchId, branch_id: SemaBranchId,
) { ) {
let branch = treehouse.tree.branch(branch_id); let branch = treehouse.tree.branch(branch_id);
if let Content::Link(linked) = &branch.attributes.content { if let Content::ResolvedLink(linked) = &branch.attributes.content {
rec_tree(treehouse, builder, linked); rec_tree(treehouse, builder, *linked);
} else { } else {
for &child_id in &branch.children { for &child_id in &branch.children {
rec_branch(treehouse, builder, child_id); rec_branch(treehouse, builder, child_id);
@ -54,12 +57,18 @@ impl NavigationMap {
} }
} }
fn rec_tree(treehouse: &Treehouse, builder: &mut NavigationMapBuilder, tree_path: &str) { fn rec_tree(treehouse: &Treehouse, builder: &mut NavigationMapBuilder, file_id: FileId) {
if let Some(roots) = treehouse.roots.get(tree_path) { if let Some(roots) = treehouse.roots.get(&file_id) {
// Pages can link to each other causing infinite recursion, so we need to handle that // Pages can link to each other causing infinite recursion, so we need to handle that
// case by skipping pages that already have been analyzed. // case by skipping pages that already have been analyzed.
if !builder.navigation_map.paths.contains_key(tree_path) { if !builder.navigation_map.paths.contains_key(&file_id) {
builder.enter_tree(tree_path.to_owned()); 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 { for &branch_id in &roots.branches {
rec_branch(treehouse, builder, branch_id); 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() builder.finish()
} }

View file

@ -30,8 +30,8 @@ pub fn branch_to_html(
return; return;
} }
let has_children = let has_children = !branch.children.is_empty()
!branch.children.is_empty() || matches!(branch.attributes.content, Content::Link(_)); || matches!(branch.attributes.content, Content::ResolvedLink(_));
let class = if has_children { "branch" } else { "leaf" }; let class = if has_children { "branch" } else { "leaf" };
let mut class = String::from(class); let mut class = String::from(class);
@ -44,7 +44,7 @@ pub fn branch_to_html(
class.push_str(" draft"); class.push_str(" draft");
} }
let component = if let Content::Link(_) = branch.attributes.content { let component = if let Content::ResolvedLink(_) = branch.attributes.content {
"b-linked" "b-linked"
} else { } else {
"b" "b"
@ -55,8 +55,9 @@ pub fn branch_to_html(
Cow::Borrowed(component) Cow::Borrowed(component)
}; };
let linked_branch = if let Content::Link(link) = &branch.attributes.content { let linked_branch = if let Content::ResolvedLink(file_id) = &branch.attributes.content {
format!(" data-th-link=\"{}\"", EscapeHtml(link)) let path = treehouse.tree_path(*file_id).expect(".tree file expected");
format!(" data-th-link=\"{}\"", EscapeHtml(path.as_str()))
} else { } else {
String::new() String::new()
}; };
@ -126,7 +127,7 @@ pub fn branch_to_html(
page_id: treehouse page_id: treehouse
.tree_path(file_id) .tree_path(file_id)
.expect(".tree file expected") .expect(".tree file expected")
.to_owned(), .to_string(),
config, config,
dirs, dirs,
@ -137,13 +138,14 @@ pub fn branch_to_html(
.render(&events, s); .render(&events, s);
let branch = treehouse.tree.branch(branch_id); 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!( write!(
s, s,
"<noscript><a class=\"navigate icon-go\" href=\"{}/{}\">Go to linked tree: <code>{}</code></a></noscript>", "<noscript><a class=\"navigate icon-go\" href=\"{}/{}\">Go to linked tree: <code>{}</code></a></noscript>",
EscapeAttribute(&config.site), EscapeAttribute(&config.site),
EscapeAttribute(link), EscapeAttribute(path.as_str()),
EscapeHtml(link), EscapeHtml(path.as_str()),
) )
.unwrap(); .unwrap();
} }
@ -151,12 +153,13 @@ pub fn branch_to_html(
s.push_str("<th-bb>"); s.push_str("<th-bb>");
{ {
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!( write!(
s, s,
"<a class=\"icon icon-go\" href=\"{}/{}\" title=\"linked tree\"></a>", "<a class=\"icon icon-go\" href=\"{}/{}\" title=\"linked tree\"></a>",
EscapeAttribute(&config.site), EscapeAttribute(&config.site),
EscapeAttribute(link), EscapeAttribute(path.as_str()),
) )
.unwrap(); .unwrap();
} else { } else {

View file

@ -8,12 +8,11 @@ use crate::state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse};
pub struct ErrorsEmitted; pub struct ErrorsEmitted;
#[instrument(skip(treehouse))] #[instrument(skip(input))]
pub fn parse_tree_with_diagnostics( pub fn parse_tree_with_diagnostics(
treehouse: &mut Treehouse,
file_id: FileId, file_id: FileId,
input: &str,
) -> Result<Roots, Vec<Diagnostic<FileId>>> { ) -> Result<Roots, Vec<Diagnostic<FileId>>> {
let input = &treehouse.source(file_id).input();
Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| { Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| {
vec![Diagnostic { vec![Diagnostic {
severity: Severity::Error, severity: Severity::Error,

View file

@ -3,23 +3,19 @@ use std::{collections::HashMap, ops::Range};
use anyhow::Context; use anyhow::Context;
use codespan_reporting::{ use codespan_reporting::{
diagnostic::{Diagnostic, Label, LabelStyle, Severity}, diagnostic::{Diagnostic, Label, LabelStyle, Severity},
files::SimpleFiles,
term::termcolor::{ColorChoice, StandardStream}, term::termcolor::{ColorChoice, StandardStream},
}; };
use tracing::instrument;
use ulid::Ulid; use ulid::Ulid;
use crate::{ use crate::{
tree::{SemaBranchId, SemaRoots, SemaTree}, tree::{SemaBranchId, SemaRoots, SemaTree},
vfs::VPathBuf, vfs::{VPath, VPathBuf},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Source { pub enum Source {
Tree { Tree { input: String, tree_path: VPathBuf },
input: String,
tree_path: String,
target_path: VPathBuf,
},
Other(String), Other(String),
} }
@ -38,26 +34,54 @@ impl AsRef<str> for Source {
} }
} }
pub type Files = SimpleFiles<String, Source>; #[derive(Debug, Clone)]
pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId; pub struct File {
pub path: VPathBuf,
pub source: Source,
pub line_starts: Vec<usize>,
}
impl File {
fn line_start(&self, line_index: usize) -> Result<usize, codespan_reporting::files::Error> {
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. /// Treehouse compilation context.
pub struct Treehouse { pub struct Treehouse {
pub files: Files, pub files: Vec<File>,
pub files_by_tree_path: HashMap<VPathBuf, FileId>,
pub tree: SemaTree, pub tree: SemaTree,
pub branches_by_named_id: HashMap<String, SemaBranchId>, pub branches_by_named_id: HashMap<String, SemaBranchId>,
pub roots: HashMap<String, SemaRoots>, pub roots: HashMap<FileId, SemaRoots>,
pub branch_redirects: HashMap<String, SemaBranchId>, pub branch_redirects: HashMap<String, SemaBranchId>,
missingno_generator: ulid::Generator, pub missingno_generator: ulid::Generator,
} }
impl Treehouse { impl Treehouse {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
files: Files::new(), files: vec![],
files_by_tree_path: HashMap::new(),
tree: SemaTree::default(), tree: SemaTree::default(),
branches_by_named_id: HashMap::new(), branches_by_named_id: HashMap::new(),
@ -69,27 +93,34 @@ impl Treehouse {
} }
} }
pub fn add_file(&mut self, filename: String, source: Source) -> FileId { pub fn add_file(&mut self, path: VPathBuf, source: Source) -> FileId {
self.files.add(filename, source) 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. /// Get the source code of a file, assuming it was previously registered.
pub fn source(&self, file_id: FileId) -> &Source { pub fn source(&self, file_id: FileId) -> &Source {
self.files &self.files[file_id.0].source
.get(file_id)
.expect("file should have been registered previously")
.source()
} }
/// Get the name of a file, assuming it was previously registered. pub fn set_source(&mut self, file_id: FileId, source: Source) {
pub fn filename(&self, file_id: FileId) -> &str { self.files[file_id.0].line_starts =
self.files codespan_reporting::files::line_starts(source.input()).collect();
.get(file_id) self.files[file_id.0].source = source;
.expect("file should have been registered previously")
.name()
} }
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) { match self.source(file_id) {
Source::Tree { tree_path, .. } => Some(tree_path), Source::Tree { tree_path, .. } => Some(tree_path),
Source::Other(_) => None, 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<Self::Name, codespan_reporting::files::Error> {
Ok(self.path(id))
}
fn source(
&'a self,
id: Self::FileId,
) -> Result<Self::Source, codespan_reporting::files::Error> {
Ok(self.source(id).input())
}
fn line_index(
&'a self,
id: Self::FileId,
byte_index: usize,
) -> Result<usize, codespan_reporting::files::Error> {
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<Range<usize>, 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 struct TomlError {
pub message: String, pub message: String,
pub span: Option<Range<usize>>, pub span: Option<Range<usize>>,
@ -135,7 +209,11 @@ pub fn toml_error_to_diagnostic(error: TomlError) -> Diagnostic<FileId> {
} }
} }
pub fn report_diagnostics(files: &Files, diagnostics: &[Diagnostic<FileId>]) -> anyhow::Result<()> { #[instrument(skip(files, diagnostics))]
pub fn report_diagnostics(
files: &Treehouse,
diagnostics: &[Diagnostic<FileId>],
) -> anyhow::Result<()> {
let writer = StandardStream::stderr(ColorChoice::Auto); let writer = StandardStream::stderr(ColorChoice::Auto);
let config = codespan_reporting::term::Config::default(); let config = codespan_reporting::term::Config::default();
for diagnostic in diagnostics { for diagnostic in diagnostics {

View file

@ -96,7 +96,7 @@ impl SemaRoots {
if successfully_parsed && attributes.title.is_empty() { if successfully_parsed && attributes.title.is_empty() {
attributes.title = match treehouse.source(file_id) { 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"), _ => 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", "note: a generated id `{}` will be used, but this id is unstable and will not persist across generations",
attributes.id 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 attributes
} }

View file

@ -2,6 +2,8 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{state::FileId, vfs::VPathBuf};
/// Top-level `%%` root attributes. /// Top-level `%%` root attributes.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct RootAttributes { 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 /// Note that `Link` branches must not contain any children. If a `Link` branch does contain
/// children, an `attribute`-type error is raised. /// 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)] #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]