strop_engine/editor/git_memory/
sidebar.rs1use std::path::Path;
5
6use strop_core::layout::printable_grapheme;
7use strop_git::memory::ChangedFile;
8use unicode_segmentation::UnicodeSegmentation;
9use unicode_width::UnicodeWidthStr;
10
11const SIDEBAR_MIN: usize = 12;
14const SIDEBAR_MAX: usize = 24;
15
16#[derive(Debug)]
19pub enum SidebarRow {
20 Dir {
21 name: String,
22 depth: usize,
23 },
24 File {
25 index: usize,
26 name: String,
27 depth: usize,
28 },
29}
30
31#[derive(Debug)]
33pub struct Sidebar {
34 rows: Vec<SidebarRow>,
35 width: usize,
37}
38
39impl Sidebar {
40 pub fn build(files: &[ChangedFile]) -> Sidebar {
45 let mut order: Vec<usize> = (0..files.len()).collect();
46 order.sort_by(|&a, &b| {
50 files[a]
51 .path
52 .as_os_str()
53 .as_encoded_bytes()
54 .cmp(files[b].path.as_os_str().as_encoded_bytes())
55 });
56 let mut rows: Vec<SidebarRow> = Vec::with_capacity(files.len() + 1);
57 let mut previous_parent = Path::new("");
58 for &index in &order {
59 let path = &files[index].path;
60 let parent = path.parent().unwrap_or_else(|| Path::new(""));
61 let shared = parent
62 .components()
63 .zip(previous_parent.components())
64 .take_while(|(left, right)| left == right)
65 .count();
66 for (depth, component) in parent.components().enumerate().skip(shared) {
67 rows.push(SidebarRow::Dir {
68 name: printable(component.as_os_str().to_string_lossy()),
69 depth,
70 });
71 }
72 rows.push(SidebarRow::File {
73 index,
74 name: path
75 .file_name()
76 .map_or_else(String::new, |name| printable(name.to_string_lossy())),
77 depth: parent.components().count(),
78 });
79 previous_parent = parent;
80 }
81 Sidebar {
82 width: Self::measured_width(files) - 1,
83 rows,
84 }
85 }
86
87 pub fn measured_width(files: &[ChangedFile]) -> usize {
89 let longest = files
90 .iter()
91 .flat_map(|file| file.path.components().enumerate())
92 .map(|(depth, component)| {
93 1 + 2 * depth + width(&component.as_os_str().to_string_lossy())
94 })
95 .max()
96 .unwrap_or(0);
97 (longest + 2).clamp(SIDEBAR_MIN, SIDEBAR_MAX) + 1
98 }
99
100 pub fn outer_width(&self) -> usize {
103 self.width + 1
104 }
105
106 pub fn width(&self) -> usize {
108 self.width
109 }
110
111 pub fn rows(&self) -> &[SidebarRow] {
113 &self.rows
114 }
115}
116
117fn grapheme_width(grapheme: &str) -> usize {
120 UnicodeWidthStr::width(printable_grapheme(grapheme))
121}
122
123fn width(text: &str) -> usize {
124 text.graphemes(true).map(grapheme_width).sum()
125}
126
127fn printable(name: std::borrow::Cow<'_, str>) -> String {
132 strop_core::layout::printable_text(name).into_owned()
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn file(path: &str) -> ChangedFile {
140 ChangedFile {
141 path: path.into(),
142 added: 1,
143 deleted: 0,
144 }
145 }
146
147 #[test]
148 fn tree_groups_by_directory_with_native_indices() {
149 let files = vec![
150 file("src/api/handlers.rs"),
151 file("README.md"),
152 file("src/main.rs"),
153 file("src/api/mod.rs"),
154 ];
155 let sidebar = Sidebar::build(&files);
156 let shape: Vec<String> = sidebar
157 .rows()
158 .iter()
159 .map(|r| match r {
160 SidebarRow::Dir { name, depth } => format!("{}{}/", " ".repeat(*depth), name),
161 SidebarRow::File { index, name, depth } => {
162 format!("{}{}#{}", " ".repeat(*depth), name, index)
163 }
164 })
165 .collect();
166 assert_eq!(
167 shape,
168 [
169 "README.md#1", "src/",
171 " api/",
172 " handlers.rs#0",
173 " mod.rs#3",
174 " main.rs#2",
175 ]
176 );
177 }
178
179 #[test]
180 fn narrow_commits_get_the_minimum_column() {
181 let files = [file("ab.rs")];
182 let sidebar = Sidebar::build(&files);
183 assert_eq!(sidebar.outer_width(), SIDEBAR_MIN + 1);
184 }
185
186 #[test]
187 fn wide_names_clamp_the_interior_at_the_maximum() {
188 let files = vec![ChangedFile {
191 path: "漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字.rs".into(),
192 added: 1,
193 deleted: 0,
194 }];
195 let sidebar = Sidebar::build(&files);
196 assert_eq!(sidebar.width(), SIDEBAR_MAX);
197 }
198}