zeta_note/
store.rs

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, &note_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                // Because 'glob' searches for a "full match" we need to add a
302                // _catch-all_ tail to all patterns
303                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}