pub mod tree_cache;
pub mod tree_context;
pub use tree_cache::{TreeCache, TreeCacheKey, TreeContextCache};
pub use tree_context::TreeContext;
use crate::file::{get_mtime, read_file_utf8};
use crate::rank::RankedEntry;
use std::collections::HashSet;
use std::path::Path;
pub fn render_tree(
abs_fname: &Path,
rel_fname: &str,
lois: &[i32],
tree_cache: &mut TreeCache,
tree_context_cache: &mut TreeContextCache,
) -> String {
let mtime = get_mtime(abs_fname).unwrap_or(0.0);
let cache_key = TreeCache::make_key(rel_fname, lois, mtime);
if let Some(cached) = tree_cache.get(&cache_key) {
return cached.clone();
}
let content = match read_file_utf8(abs_fname) {
Some(c) => c,
None => return String::new(),
};
let content = if content.ends_with('\n') {
content
} else {
format!("{}\n", content)
};
let ctx = tree_context_cache.get_or_create(rel_fname, abs_fname, &content, mtime);
ctx.reset_lois();
ctx.add_lines_of_interest(lois);
ctx.add_context();
let result = ctx.format();
tree_cache.set(cache_key, result.clone());
result
}
pub fn to_tree(
ranked_tags: &[RankedEntry],
chat_rel_fnames: &HashSet<String>,
max_line_length: usize,
tree_cache: &mut TreeCache,
tree_context_cache: &mut TreeContextCache,
) -> String {
if ranked_tags.is_empty() {
return String::new();
}
let mut sorted: Vec<&RankedEntry> = ranked_tags.iter().collect();
sorted.sort_by_key(|e| e.rel_fname());
let sentinel = sentinel_entry();
sorted.push(&sentinel);
let mut output = String::new();
let mut cur_fname: Option<&str> = None;
let mut cur_abs_fname: Option<&str> = None;
let mut lois: Option<Vec<i32>> = None;
for entry in &sorted {
let entry_fname = entry.rel_fname();
if chat_rel_fnames.contains(entry_fname) {
continue;
}
if Some(entry_fname) != cur_fname || is_sentinel(entry) {
if let Some(fname) = cur_fname {
if let Some(ref loi_list) = lois {
output.push('\n');
output.push_str(fname);
output.push_str(":\n");
if let Some(abs) = cur_abs_fname {
let rendered = render_tree(
Path::new(abs),
fname,
loi_list,
tree_cache,
tree_context_cache,
);
output.push_str(&rendered);
}
} else {
output.push('\n');
output.push_str(fname);
output.push('\n');
}
}
if !is_sentinel(entry) {
cur_fname = Some(entry_fname);
match entry {
RankedEntry::Tagged { tags, .. } => {
lois = Some(Vec::new());
cur_abs_fname = tags.first().map(|t| t.fname.as_str());
}
RankedEntry::Bare { .. } => {
lois = None;
cur_abs_fname = None;
}
}
}
}
if !is_sentinel(entry)
&& let (Some(loi_list), RankedEntry::Tagged { tags, .. }) = (&mut lois, *entry)
{
for tag in tags {
loi_list.push(tag.line);
}
}
}
let output = truncate_lines(&output, max_line_length);
if output.is_empty() || output.ends_with('\n') {
output
} else {
format!("{}\n", output)
}
}
fn sentinel_entry() -> RankedEntry {
RankedEntry::Bare {
rel_fname: "\x00SENTINEL\x00".to_string(),
score: 0.0,
}
}
fn is_sentinel(entry: &RankedEntry) -> bool {
entry.rel_fname() == "\x00SENTINEL\x00"
}
fn truncate_lines(text: &str, max_length: usize) -> String {
text.lines()
.map(|line| {
if line.chars().count() > max_length {
line.chars().take(max_length).collect::<String>()
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ if text.ends_with('\n') { "\n" } else { "" }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tag::Tag;
#[allow(dead_code)]
fn make_tagged(rel: &str, abs: &str, lines: &[i32]) -> RankedEntry {
let tags: Vec<Tag> = lines
.iter()
.map(|&l| Tag::def(rel, abs, l, "test"))
.collect();
RankedEntry::Tagged {
rel_fname: rel.to_string(),
ident: "test".to_string(),
tags,
score: 1.0,
}
}
fn make_bare(rel: &str) -> RankedEntry {
RankedEntry::Bare {
rel_fname: rel.to_string(),
score: 0.0,
}
}
#[test]
fn to_tree_empty() {
let mut tc = TreeCache::new();
let mut tcc = TreeContextCache::new();
let result = to_tree(&[], &HashSet::new(), 100, &mut tc, &mut tcc);
assert!(result.is_empty());
}
#[test]
fn to_tree_bare_entry() {
let entries = vec![make_bare("test.rs")];
let mut tc = TreeCache::new();
let mut tcc = TreeContextCache::new();
let result = to_tree(&entries, &HashSet::new(), 100, &mut tc, &mut tcc);
assert!(result.contains("test.rs"));
assert!(!result.contains(":")); }
#[test]
fn to_tree_excludes_chat() {
let entries = vec![make_bare("chat.rs"), make_bare("other.rs")];
let mut chat = HashSet::new();
chat.insert("chat.rs".to_string());
let mut tc = TreeCache::new();
let mut tcc = TreeContextCache::new();
let result = to_tree(&entries, &chat, 100, &mut tc, &mut tcc);
assert!(!result.contains("chat.rs"));
assert!(result.contains("other.rs"));
}
#[test]
fn truncate_lines_basic() {
let text = "short\nthis line is quite long\n";
let result = truncate_lines(text, 10);
assert!(result.lines().all(|l| l.len() <= 10));
}
}