Skip to main content

indigo_core/
buffer.rs

1use crate::{fs::Fs, syntax::Language, text::Text};
2use camino::Utf8Path;
3use ropey::Rope;
4use std::sync::Arc;
5
6slotmap::new_key_type! {
7    #[must_use]
8    pub struct BufferKey;
9}
10
11#[derive(Clone, Default)]
12pub enum BufferKind {
13    /// In-memory scratch buffer.
14    #[default]
15    Scratch,
16    /// Buffer mapping to a file on disk.
17    File {
18        path: Arc<Utf8Path>,
19        /// Last known state of file on disk, used for modification tracking.
20        on_disk: Rope,
21    },
22}
23
24#[derive(Clone, Default)]
25pub struct Buffer {
26    kind: BufferKind,
27    pub text: Text,
28}
29
30impl Buffer {
31    #[must_use]
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn open(fs: &impl Fs, path: impl AsRef<Utf8Path>) -> anyhow::Result<Self> {
37        let path = path.as_ref();
38        let exists = fs.exists(path)?;
39        let rope = if exists {
40            let bytes = fs.read(path)?;
41            let string = str::from_utf8(&bytes)?;
42            Rope::from(string)
43        } else {
44            Rope::new()
45        };
46        let mut buffer = Self::from(rope);
47        // Using the normalized rope (with trailing newline appended if missing) instead of the
48        // original so it doesn't appear already modified.
49        buffer.kind = BufferKind::File {
50            path: Arc::from(path),
51            on_disk: buffer.text.rope().clone(),
52        };
53        if let Ok(language) = Language::try_from(path) {
54            buffer.text.set_language(language);
55        }
56        Ok(buffer)
57    }
58
59    pub fn save(&mut self, fs: &impl Fs) -> anyhow::Result<()> {
60        if let BufferKind::File { path, on_disk } = &mut self.kind {
61            let mut bytes = Vec::with_capacity(self.text.len());
62            self.text.write_to(&mut bytes)?;
63            fs.write(path, &bytes)?;
64            *on_disk = self.text.rope().clone();
65        }
66        Ok(())
67    }
68
69    pub fn save_as(&mut self, fs: &impl Fs, path: impl AsRef<Utf8Path>) -> anyhow::Result<()> {
70        let path = path.as_ref();
71        let mut bytes = Vec::with_capacity(self.text.len());
72        self.text.write_to(&mut bytes)?;
73        fs.write(path, &bytes)?;
74        self.kind = BufferKind::File {
75            path: Arc::from(path),
76            on_disk: self.text.rope().clone(),
77        };
78        Ok(())
79    }
80
81    #[must_use]
82    pub fn kind(&self) -> &BufferKind {
83        &self.kind
84    }
85
86    #[must_use]
87    pub fn path(&self) -> Option<&Utf8Path> {
88        if let BufferKind::File { path, .. } = &self.kind {
89            Some(path)
90        } else {
91            None
92        }
93    }
94
95    #[must_use]
96    pub fn is_modified(&self) -> Option<bool> {
97        if let BufferKind::File { on_disk, .. } = &self.kind {
98            Some(self.text.rope() != on_disk)
99        } else {
100            None
101        }
102    }
103
104    pub fn assert_invariants(&self) -> anyhow::Result<()> {
105        self.text.assert_invariants()
106    }
107}
108
109impl From<Rope> for Buffer {
110    fn from(rope: Rope) -> Self {
111        Self::from(Text::from(rope))
112    }
113}
114
115impl From<Text> for Buffer {
116    fn from(text: Text) -> Self {
117        Self {
118            text,
119            ..Self::default()
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::fs::TestFs;
128
129    #[test]
130    fn fs() -> anyhow::Result<()> {
131        let fs = TestFs::default();
132        fs.write("main.rs".into(), b"fn main() {}")?;
133        let mut buffer = Buffer::open(&fs, "main.rs")?;
134        // Text normalization appends the missing trailing newline on open.
135        assert_eq!(&buffer.text.to_string(), "fn main() {}\n");
136        assert_eq!(buffer.is_modified(), Some(false));
137        let BufferKind::File { path, .. } = &mut buffer.kind else {
138            unreachable!();
139        };
140        *path = Arc::from(Utf8Path::new("main2.rs"));
141        buffer.save(&fs)?;
142        assert_eq!(fs.read("main2.rs".into())?, b"fn main() {}\n");
143        Ok(())
144    }
145}