1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
use std::mem;

pub use monitor::{
    Monitor, MonitorConfig, MonitorDirectories, MonitorEntry, MonitorMessage, NotifyMonitor,
};

use mun_paths::{AbsPath, AbsPathBuf};
use path_interner::PathInterner;

mod monitor;
mod path_interner;

/// A `FileId` represents a unique identifier for a file within the `VirtualFileSystem`.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FileId(pub u32);

/// The `VirtualFileSystem` is a struct that manages a set of files and their content. Changes to
/// the instance are logged, they can be be retrieved via the `take_changes` method.
#[derive(Default)]
pub struct VirtualFileSystem {
    /// Used to convert from paths to `FileId` and vice versa.
    interner: PathInterner,

    /// Per file the content of the file, or `None` if no content is available
    file_contents: Vec<Option<Vec<u8>>>,

    /// A record of changes to this instance.
    changes: Vec<ChangedFile>,
}

/// A record of a change to a file
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChangedFile {
    pub file_id: FileId,
    pub kind: ChangeKind,
}

impl ChangedFile {
    /// Returns true if this change indicates that the file was created or deleted
    pub fn is_created_or_deleted(&self) -> bool {
        matches!(self.kind, ChangeKind::Create | ChangeKind::Delete)
    }
}

/// The type of change that a file undergoes
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ChangeKind {
    Create,
    Modify,
    Delete,
}

impl VirtualFileSystem {
    /// Returns `true` if there are changes that can be processed.
    pub fn has_changes(&self) -> bool {
        !self.changes.is_empty()
    }

    /// Returns the changes performed on the instance since the last time this function was called
    /// or since the creation of the instance.
    pub fn take_changes(&mut self) -> Vec<ChangedFile> {
        mem::take(&mut self.changes)
    }

    /// Returns the `FileId` of the file at the specified `path` or `None` if there is no data for
    /// that file.
    pub fn file_id(&self, path: &AbsPath) -> Option<FileId> {
        self.interner
            .get(path)
            .filter(|&file_id| self.get(file_id).is_some())
    }

    /// Returns the path of the file with the specified `FileId`.
    pub fn file_path(&self, file_id: FileId) -> &AbsPath {
        self.interner.lookup(file_id)
    }

    /// Returns the content of the file with the specified `FileId`.
    pub fn file_contents(&self, file_id: FileId) -> Option<&[u8]> {
        self.get(file_id).as_deref()
    }

    /// Returns an iterator that iterates all `FileId`s and their path.
    pub fn iter(&self) -> impl Iterator<Item = (FileId, &AbsPath)> + '_ {
        self.file_contents
            .iter()
            .enumerate()
            .filter(|(_, contents)| contents.is_some())
            .map(move |(id, _)| {
                let file_id = FileId(id as u32);
                let path = self.interner.lookup(file_id);
                (file_id, path)
            })
    }

    /// Notifies this instance that the contents of the specified file has changed to something
    /// else. Returns true if the new contents is actually different.
    pub fn set_file_contents(&mut self, path: &AbsPath, contents: Option<Vec<u8>>) -> bool {
        let file_id = self.alloc_file_id(path);
        let kind = match (&self.get(file_id), &contents) {
            (None, None) => return false,
            (None, Some(_)) => ChangeKind::Create,
            (Some(_), None) => ChangeKind::Delete,
            (Some(old), Some(new)) if old == new => return false,
            (Some(_), Some(_)) => ChangeKind::Modify,
        };

        *self.get_mut(file_id) = contents;
        self.changes.push(ChangedFile { file_id, kind });
        true
    }

    /// Returns the `FileId` for the specified path and ensures that we can use it with this
    /// instance.
    fn alloc_file_id(&mut self, path: &AbsPath) -> FileId {
        let file_id = self.interner.intern(path);
        let idx = file_id.0 as usize;
        let len = self.file_contents.len().max(idx + 1);
        self.file_contents.resize(len, None);
        file_id
    }

    /// Returns a reference to the current content of a specific file. This function is only used
    /// internally. Use the `file_contents` function to get the contents of a file.
    fn get(&self, file_id: FileId) -> &Option<Vec<u8>> {
        &self.file_contents[file_id.0 as usize]
    }

    /// Returns a mutable reference to the current content of a specific file. This function is only
    /// used internally. Use the `set_file_contents` function to update the contents of a file.
    fn get_mut(&mut self, file_id: FileId) -> &mut Option<Vec<u8>> {
        &mut self.file_contents[file_id.0 as usize]
    }
}

#[cfg(test)]
mod tests {
    use std::convert::TryInto;
    use std::path::PathBuf;

    use crate::{AbsPathBuf, ChangeKind, ChangedFile, VirtualFileSystem};

    #[test]
    fn vfs() {
        let mut vfs = VirtualFileSystem::default();
        assert!(!vfs.has_changes());

        // Construct a fake file name
        let abs_manifest_dir: AbsPathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .try_into()
            .unwrap();
        let test_path = abs_manifest_dir.as_path().join("test");

        // We should not have a FileId for this file yet
        assert!(vfs.file_id(&test_path).is_none());

        // Store some data in the vfs, this should definitly trigger a change
        assert!(vfs.set_file_contents(&test_path, Some(vec![])));
        assert!(vfs.has_changes());

        // We should now have a FileId
        let file_id = vfs
            .file_id(&test_path)
            .expect("there should be a FileId by now");

        // Lookup the path, it should match
        assert_eq!(&test_path, vfs.file_path(file_id));

        // Get the contents of the file
        assert!(vfs.file_contents(file_id).is_some());

        // Modify the file contents, but dont actually modify it, should not trigger a change
        assert!(!vfs.set_file_contents(&test_path, Some(vec![])));

        // Actually modify the contents
        assert!(vfs.set_file_contents(&test_path, Some(vec![0])));

        // Remove the file contents, should also trigger a change
        assert!(vfs.set_file_contents(&test_path, None));

        // We should now no longer have a file id because the contents was removed
        assert_eq!(vfs.file_id(&test_path), None);

        // Get the changes
        assert!(vfs.has_changes());
        assert_eq!(
            vfs.take_changes(),
            vec![
                ChangedFile {
                    file_id,
                    kind: ChangeKind::Create
                },
                ChangedFile {
                    file_id,
                    kind: ChangeKind::Modify
                },
                ChangedFile {
                    file_id,
                    kind: ChangeKind::Delete
                },
            ]
        );
    }

    #[test]
    fn iter() {
        let mut vfs = VirtualFileSystem::default();

        // Construct a fake file name
        let abs_manifest_dir: AbsPathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .try_into()
            .unwrap();

        // Add two files to the system
        let test_path2 = abs_manifest_dir.as_path().join("test2");
        let test_path = abs_manifest_dir.as_path().join("test");
        assert!(vfs.set_file_contents(&test_path, Some(vec![0])));
        assert!(vfs.set_file_contents(&test_path2, Some(vec![1])));
        let file_id = vfs.file_id(&test_path).unwrap();
        let file_id2 = vfs.file_id(&test_path2).unwrap();
        assert_ne!(file_id, file_id2);

        let mut entries = vfs
            .iter()
            .map(|(id, entry)| (id, entry.to_path_buf()))
            .collect::<Vec<_>>();
        let mut expected_entries =
            vec![(file_id, test_path.clone()), (file_id2, test_path2.clone())];

        entries.sort_by_key(|entry| entry.0);
        expected_entries.sort_by_key(|entry| entry.0);

        assert_eq!(entries, expected_entries);
    }
}