1use anyhow::Result;
2
3use glob::Pattern;
4use lsp_types::WorkspaceFolder;
5
6use std::{
7 collections::HashSet,
8 path::{Path, PathBuf},
9 sync::Arc,
10 time::SystemTime,
11};
12use tokio::fs;
13
14use tracing::debug;
15
16use crate::{
17 facts::{self, FactsDB},
18 store,
19 structure::{NoteID, NoteName},
20};
21
22#[derive(Default)]
23pub struct Workspace {
24 pub folders: Vec<(NoteFolder, FactsDB, Vec<Pattern>)>,
25}
26
27impl Workspace {
28 pub async fn new(input_folders: &[NoteFolder]) -> Result<Workspace> {
29 let mut workspace = Workspace::default();
30 for f in input_folders {
31 workspace.add_folder(f.clone()).await?;
32 }
33
34 Ok(workspace)
35 }
36
37 pub fn note_count(&self) -> usize {
38 self.folders
39 .iter()
40 .map(|(_, facts, _)| facts.note_index().size())
41 .sum()
42 }
43
44 pub fn owning_folder(&self, file: &Path) -> Option<(&NoteFolder, &FactsDB)> {
45 self.folders
46 .iter()
47 .find(|(folder, _, _)| file.starts_with(&folder.root))
48 .map(|(folder, facts, _)| (folder, facts))
49 }
50
51 pub fn owning_folder_mut(
52 &mut self,
53 file: &Path,
54 ) -> Option<(&mut NoteFolder, &mut FactsDB, &[Pattern])> {
55 self.folders
56 .iter_mut()
57 .find(|(folder, _, _)| file.starts_with(&folder.root))
58 .map(|(folder, facts, ignores)| (folder, facts, ignores.as_slice()))
59 }
60
61 pub fn remove_folder(&mut self, path: &Path) {
62 if let Some((idx, _)) = self
63 .folders
64 .iter()
65 .enumerate()
66 .find(|(_, (folder, _, _))| folder.root == path)
67 {
68 self.folders.remove(idx);
69 }
70 }
71
72 pub async fn add_folder(&mut self, folder: NoteFolder) -> Result<()> {
73 if self.owning_folder(&folder.root).is_some() {
74 return Ok(());
75 }
76
77 let ignores = store::find_ignores(&folder.root).await?;
78 let note_files = store::find_notes(&folder.root, &ignores).await?;
79 debug!(
80 "Workspace {}: found {} note files",
81 folder.root.display(),
82 note_files.len()
83 );
84 let facts = facts::FactsDB::from_files(&folder.root, ¬e_files, &ignores).await?;
85 self.folders.push((folder, facts, ignores));
86 Ok(())
87 }
88}
89
90#[derive(Debug, Clone)]
91pub struct NoteFolder {
92 pub root: PathBuf,
93 pub name: String,
94}
95
96impl NoteFolder {
97 pub fn from_workspace_folder(workspace_folder: &WorkspaceFolder) -> NoteFolder {
98 NoteFolder {
99 root: workspace_folder
100 .uri
101 .to_file_path()
102 .expect("Failed to turn URI into a path"),
103 name: workspace_folder.name.clone(),
104 }
105 }
106
107 pub fn from_root_path(root: &Path) -> NoteFolder {
108 NoteFolder {
109 root: root.to_path_buf(),
110 name: root
111 .file_name()
112 .map(|s| s.to_string_lossy().to_string())
113 .unwrap_or_else(|| root.to_string_lossy().to_string()),
114 }
115 }
116}
117
118#[derive(Debug, PartialEq, Eq, Clone, Hash)]
119pub struct NoteFile {
120 pub root: Arc<Path>,
121 pub path: Arc<Path>,
122 pub name: Arc<NoteName>,
123}
124
125impl NoteFile {
126 pub fn new(root: &Path, path: &Path) -> Self {
127 let name: NoteName = NoteName::from_path(path, root);
128
129 Self {
130 root: root.into(),
131 path: path.into(),
132 name: name.into(),
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct NoteIndex {
139 notes: Arc<[NoteFile]>,
140}
141
142impl Default for NoteIndex {
143 fn default() -> Self {
144 Self {
145 notes: Vec::new().into(),
146 }
147 }
148}
149
150impl NoteIndex {
151 pub fn size(&self) -> usize {
152 self.notes.len()
153 }
154
155 pub fn ids(&self) -> impl Iterator<Item = NoteID> {
156 (0..self.notes.len()).into_iter().map(|i| i.into())
157 }
158
159 pub fn files(&self) -> impl Iterator<Item = &NoteFile> {
160 self.notes.iter()
161 }
162
163 pub fn find_by_path(&self, path: &Path) -> Option<NoteID> {
164 self.notes.iter().enumerate().find_map(|(idx, nf)| {
165 if nf.path.as_ref() == path {
166 Some(idx.into())
167 } else {
168 None
169 }
170 })
171 }
172
173 pub fn find_by_name(&self, name: &NoteName) -> Option<NoteID> {
174 self.notes.iter().enumerate().find_map(|(idx, nf)| {
175 if NoteName::from_path(&nf.path, &nf.root) == *name {
176 Some(idx.into())
177 } else {
178 None
179 }
180 })
181 }
182
183 pub fn find_by_id(&self, id: NoteID) -> NoteFile {
184 self.notes[id.to_usize()].clone()
185 }
186
187 pub fn with_note_file(&self, file: NoteFile) -> NoteIndex {
188 let mut notes: HashSet<NoteFile> = self
189 .notes
190 .iter()
191 .map(|x| x.to_owned())
192 .collect::<HashSet<_>>();
193 notes.insert(file);
194
195 let notes = notes.into_iter().collect::<Vec<_>>();
196 NoteIndex {
197 notes: notes.into(),
198 }
199 }
200}
201
202#[derive(Debug, PartialEq, Eq, Clone)]
203pub struct NoteText {
204 pub version: Version,
205 pub content: Arc<str>,
206}
207
208impl NoteText {
209 pub fn new(version: Version, content: Arc<str>) -> Self {
210 Self { version, content }
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum Version {
216 Fs(SystemTime),
217 Vs(i32),
218}
219
220impl Version {
221 pub fn to_lsp_version(&self) -> Option<i32> {
222 match self {
223 Version::Vs(v) => Some(*v),
224 _ => None,
225 }
226 }
227}
228
229pub async fn read_note(path: &Path, root: &Path, ignores: &[Pattern]) -> Result<Option<NoteText>> {
230 if is_note_file(path, root, ignores) {
231 let content = fs::read_to_string(path).await?;
232 let meta = fs::metadata(path).await?;
233 let version = Version::Fs(meta.modified()?);
234
235 Ok(Some(NoteText::new(version, content.into())))
236 } else {
237 Ok(None)
238 }
239}
240
241pub async fn find_notes(root_path: &Path, ignores: &[Pattern]) -> Result<Vec<PathBuf>> {
242 find_notes_inner(root_path, ignores).await
243}
244
245async fn find_notes_inner<'a>(root_path: &Path, ignores: &[Pattern]) -> Result<Vec<PathBuf>> {
246 let mut remaining_dirs = vec![root_path.to_path_buf()];
247 let mut found_files = vec![];
248 while let Some(dir_path) = remaining_dirs.pop() {
249 let mut dir_contents = fs::read_dir(dir_path).await?;
250 while let Some(entry) = dir_contents.next_entry().await? {
251 let entry_type = entry.file_type().await?;
252 let entry_path = entry.path();
253 if entry_type.is_file() && is_note_file(&entry_path, root_path, ignores) {
254 found_files.push(entry_path);
255 } else if entry_type.is_dir() {
256 remaining_dirs.push(entry_path);
257 }
258 }
259 }
260 Ok(found_files)
261}
262
263fn is_note_file(path: &Path, root: &Path, ignores: &[Pattern]) -> bool {
264 let path_str = match path.strip_prefix(root).ok().and_then(|p| p.to_str()) {
265 Some(str) => str,
266 _ => return false,
267 };
268 let is_md = path
269 .extension()
270 .filter(|ext| ext.to_string_lossy().to_lowercase() == "md")
271 .is_some();
272 if !is_md {
273 return false;
274 }
275
276 for pat in ignores {
277 if pat.matches(path_str) {
278 return false;
279 }
280 }
281
282 true
283}
284
285pub async fn find_ignores(root_path: &Path) -> Result<Vec<Pattern>> {
286 let supported_ignores = [".ignore", ".gitignore"];
287
288 for ignore in &supported_ignores {
289 let file = root_path.join(ignore);
290
291 if file.exists() {
292 debug!("Found ignore file: {}", file.display());
293
294 let content = fs::read_to_string(file).await?;
295 let mut patterns = Vec::new();
296 for line in content.lines() {
297 if let Ok(pat) = Pattern::new(line) {
298 patterns.push(pat);
299 }
300
301 let rest_pattern = if line.ends_with('/') {
304 line.to_string() + "**/*"
305 } else {
306 line.to_string() + "/**/*"
307 };
308 if let Ok(pat) = Pattern::new(&rest_pattern) {
309 patterns.push(pat);
310 }
311 }
312 debug!("Found {} ignore patterns", patterns.len());
313
314 return Ok(patterns);
315 }
316 }
317
318 debug!("Found no ignore file");
319 Ok(vec![])
320}