use std::path::{Path, PathBuf};
use termesh_filesystem::FileTree;
use crate::root::{ProjectKind, WorkspaceRoot};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeEntry {
pub path: PathBuf,
pub depth: usize,
pub is_dir: bool,
pub unexplored: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceSnapshot {
pub root: PathBuf,
pub project_kind: ProjectKind,
pub project_kinds: Vec<ProjectKind>,
pub visible_tree: Vec<TreeEntry>,
pub selection: Option<PathBuf>,
}
impl WorkspaceSnapshot {
pub fn build(root: &WorkspaceRoot, tree: &FileTree) -> Self {
let rows = tree.visible_rows();
let selected = tree.selected();
let visible_tree = rows
.iter()
.filter(|r| r.id != tree.root())
.filter_map(|r| {
let full = tree.path_of(r.id)?;
Some(TreeEntry {
path: relative_to(&root.path, full),
depth: r.depth.saturating_sub(1),
is_dir: r.is_expandable,
unexplored: r.is_expandable && !r.expanded,
})
})
.collect();
let selection = tree
.path_of(selected)
.filter(|_| selected != tree.root())
.map(|p| relative_to(&root.path, p));
Self {
root: root.path.clone(),
project_kind: root.kind,
project_kinds: root.kinds.clone(),
visible_tree,
selection,
}
}
pub fn len(&self) -> usize {
self.visible_tree.len()
}
pub fn is_empty(&self) -> bool {
self.visible_tree.is_empty()
}
}
fn relative_to(root: &Path, path: &Path) -> PathBuf {
path.strip_prefix(root).unwrap_or(path).to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
use termesh_filesystem::{DirEntryInfo, EntryKind};
fn root() -> WorkspaceRoot {
WorkspaceRoot {
path: PathBuf::from("/proj"),
kind: ProjectKind::Rust,
kinds: vec![ProjectKind::Rust],
detected: true,
}
}
fn entry(name: &str, kind: EntryKind) -> DirEntryInfo {
DirEntryInfo { name: name.into(), path: PathBuf::from("/proj").join(name), kind }
}
fn tree() -> FileTree {
let mut t = FileTree::new("/proj", "proj");
let _ = t.expand(t.root());
t.set_children(
t.root(),
vec![entry("src", EntryKind::Dir), entry("Cargo.toml", EntryKind::File)],
);
t
}
fn paths(s: &WorkspaceSnapshot) -> Vec<String> {
s.visible_tree.iter().map(|e| e.path.to_string_lossy().into_owned()).collect()
}
#[test]
fn the_snapshot_carries_the_root_and_project_kind() {
let s = WorkspaceSnapshot::build(&root(), &tree());
assert_eq!(s.root, Path::new("/proj"));
assert_eq!(s.project_kind, ProjectKind::Rust);
assert_eq!(s.project_kinds, vec![ProjectKind::Rust]);
}
#[test]
fn paths_are_relative_to_the_root() {
let s = WorkspaceSnapshot::build(&root(), &tree());
assert_eq!(paths(&s), ["src", "Cargo.toml"]);
assert!(
!paths(&s).iter().any(|p| p.starts_with('/')),
"absolute paths would leak the user's home directory into agent context"
);
}
#[test]
fn the_root_row_itself_is_not_repeated_as_an_entry() {
let s = WorkspaceSnapshot::build(&root(), &tree());
assert!(!paths(&s).contains(&"proj".to_string()));
}
#[test]
fn unopened_directories_are_flagged_as_unexplored() {
let s = WorkspaceSnapshot::build(&root(), &tree());
let src = s.visible_tree.iter().find(|e| e.path == Path::new("src")).unwrap();
assert!(src.is_dir);
assert!(src.unexplored, "the agent must be able to ask for what it cannot see");
let toml = s.visible_tree.iter().find(|e| e.path == Path::new("Cargo.toml")).unwrap();
assert!(!toml.unexplored, "files are never unexplored");
}
#[test]
fn an_expanded_directory_is_not_unexplored_and_its_children_appear() {
let mut t = tree();
let src = t.visible_rows()[1].id;
let _ = t.expand(src);
t.set_children(
src,
vec![DirEntryInfo {
name: "main.rs".into(),
path: "/proj/src/main.rs".into(),
kind: EntryKind::File,
}],
);
let s = WorkspaceSnapshot::build(&root(), &t);
assert_eq!(paths(&s), ["src", "src/main.rs", "Cargo.toml"]);
assert!(!s.visible_tree[0].unexplored);
}
#[test]
fn depth_is_rebased_so_the_roots_children_are_zero() {
let mut t = tree();
let src = t.visible_rows()[1].id;
let _ = t.expand(src);
t.set_children(
src,
vec![DirEntryInfo {
name: "main.rs".into(),
path: "/proj/src/main.rs".into(),
kind: EntryKind::File,
}],
);
let s = WorkspaceSnapshot::build(&root(), &t);
assert_eq!(s.visible_tree[0].depth, 0, "src");
assert_eq!(s.visible_tree[1].depth, 1, "src/main.rs");
}
#[test]
fn the_selection_is_reported_relative_to_the_root() {
let mut t = tree();
let toml = t.visible_rows()[2].id;
t.select(toml);
let s = WorkspaceSnapshot::build(&root(), &t);
assert_eq!(s.selection, Some(PathBuf::from("Cargo.toml")));
}
#[test]
fn selecting_the_root_reports_no_selection() {
let s = WorkspaceSnapshot::build(&root(), &tree());
assert_eq!(s.selection, None, "the root is not a meaningful selection");
}
#[test]
fn the_agent_sees_exactly_the_visible_rows_no_more() {
let t = tree();
let s = WorkspaceSnapshot::build(&root(), &t);
assert_eq!(
s.len(),
t.visible_rows().len() - 1,
"every visible row but the root, and nothing else"
);
}
#[test]
fn a_collapsed_directory_hides_its_children_from_the_agent_too() {
let mut t = tree();
let src = t.visible_rows()[1].id;
let _ = t.expand(src);
t.set_children(
src,
vec![DirEntryInfo {
name: "main.rs".into(),
path: "/proj/src/main.rs".into(),
kind: EntryKind::File,
}],
);
t.collapse(src);
let s = WorkspaceSnapshot::build(&root(), &t);
assert_eq!(paths(&s), ["src", "Cargo.toml"]);
assert!(s.visible_tree[0].unexplored, "collapsed reads as unexplored");
}
}