duat_core/context/
cache.rs

1//! Caching utilities for Duat
2//!
3//! The cache in Duat is used when restoring previous information from
4//! a previous Duat instance, or when reloading Duat's configuration
5//! crate.
6//!
7//! One example for where this is used is the [Ui], which has
8//! information about how much a given buffer should be scrolled when
9//! opening it in Duat. Another example is [`Cursors`], which stores a
10//! list of open [`Cursor`]s, to start the buffer on.
11//!
12//! Plugins are able to cache any type that implements [`Encode`]
13//! and [`Decode`]. Duat provides these traits from the [`bincode`]
14//! crate, but in order to derive them, you will need a small addition
15//! on the declaration of the type:
16//!
17//! ```rust
18//! use duat_core::context::{Decode, Encode};
19//! #[derive(Encode, Decode)]
20//! #[bincode(crate = "duat_core::context::bincode")]
21//! enum MyCacheableEnum {
22//!     // ...
23//! }
24//! ```
25//!
26//! [Ui]: crate::ui::traits::RawUi
27//! [`Cursors`]: crate::mode::Cursors
28//! [`Cursor`]: crate::mode::Cursor
29//! [`Point`]: crate::text::Point
30use std::{
31    ffi::OsString,
32    fs::File,
33    hash::{DefaultHasher, Hash, Hasher},
34    marker::PhantomData,
35    path::{Path, PathBuf},
36};
37
38pub use bincode::{self, Decode, Encode};
39
40use self::bincode::{
41    config::{Configuration, Fixint, LittleEndian, NoLimit},
42    encode_into_std_write,
43};
44use crate::{
45    text::{Text, txt},
46    utils::{duat_name, src_crate},
47};
48
49/// Used in order to cache things
50pub struct Cache(PhantomData<()>);
51
52impl Cache {
53    /// Returns a new instance of [`Cache`]
54    pub(crate) fn new() -> Self {
55        Self(PhantomData)
56    }
57
58    /// Tries to load the cache stored by Duat for the given type
59    ///
60    /// The cache must have been previously stored by
61    /// [`Cache::store`]. If it does not exist, or the buffer can't
62    /// be correctly interpreted, returns [`None`]
63    pub fn load<C: Decode<()> + Default + 'static>(
64        &self,
65        path: impl AsRef<Path>,
66    ) -> Result<C, Text> {
67        let mut cache_file = cache_file::<C>(path.as_ref(), false)?;
68
69        if cache_file.metadata()?.len() == 0 {
70            return Ok(C::default());
71        }
72
73        let config = Configuration::<LittleEndian, Fixint, NoLimit>::default();
74        bincode::decode_from_std_read(&mut cache_file, config).map_err(|err| txt!("{err}"))
75    }
76
77    /// Stores the cache for the given type for that buffer
78    ///
79    /// The cache will be stored under
80    /// `$cache/duat/{base64_path}:{file_name}/{crate}::{type}`.
81    /// The cache can then later be loaded by [`Cache::load`].
82    pub fn store<C: Encode + 'static>(
83        &self,
84        path: impl AsRef<Path>,
85        cache: C,
86    ) -> Result<usize, Text> {
87        let mut cache_file = cache_file::<C>(path.as_ref(), true)?;
88
89        let config = Configuration::<LittleEndian, Fixint, NoLimit>::default();
90        encode_into_std_write(cache, &mut cache_file, config).map_err(|err| txt!("{err}"))
91    }
92
93    /// Deletes the cache for all types for `path`
94    ///
95    /// This is done if the buffer no longer exists, in order to
96    /// prevent incorrect storage.
97    pub fn delete(&self, path: impl Into<PathBuf>) {
98        fn delete_cache_inner(path: PathBuf) {
99            let (Some(cache_dir), Some(file_name)) = (dirs_next::cache_dir(), path.file_name())
100            else {
101                return;
102            };
103
104            let mut hasher = DefaultHasher::new();
105            path.hash(&mut hasher);
106            let hash_value = hasher.finish();
107
108            let cached_file_name = {
109                let mut name = OsString::from(format!("{hash_value}-"));
110                name.push(file_name);
111                name
112            };
113
114            let src = cache_dir
115                .join("duat")
116                .join("structs")
117                .join(cached_file_name);
118            // It could fail if the directory doesn't exist, but we don't really
119            // care.
120            let _ = std::fs::remove_dir_all(src);
121        }
122
123        delete_cache_inner(path.into());
124    }
125
126    /// Deletes the cache for everything related to the given `path`
127    pub fn delete_for<C: 'static>(&self, path: impl AsRef<Path>) {
128        let path = path.as_ref();
129        let (Some(cache_dir), Some(file_name)) = (dirs_next::cache_dir(), path.file_name()) else {
130            return;
131        };
132
133        let mut hasher = DefaultHasher::new();
134        path.hash(&mut hasher);
135        let hash_value = hasher.finish();
136
137        let cached_file_name = {
138            let mut name = OsString::from(format!("{hash_value}-"));
139            name.push(file_name);
140            name
141        };
142
143        let src = cache_dir
144            .join("duat")
145            .join("structs")
146            .join(cached_file_name)
147            .join(format!("{}-{}", src_crate::<C>(), duat_name::<C>()));
148
149        if let Ok(true) = src.try_exists() {
150            std::fs::remove_file(src).unwrap();
151        }
152    }
153}
154
155fn cache_file<C: 'static>(path: &Path, truncate: bool) -> std::io::Result<File> {
156    let mut hasher = DefaultHasher::new();
157    path.hash(&mut hasher);
158    let hash_value = hasher.finish();
159
160    let cached_file_name = {
161        let mut name = OsString::from(format!("{hash_value}-"));
162        name.push(path.file_name().ok_or_else(|| {
163            std::io::Error::new(
164                std::io::ErrorKind::IsADirectory,
165                format!("{path:?} is a directory, not a buffer for caching"),
166            )
167        })?);
168        name
169    };
170
171    let src_dir = dirs_next::cache_dir()
172        .ok_or_else(|| {
173            std::io::Error::new(std::io::ErrorKind::NotFound, "cache directory isn't set")
174        })?
175        .join("duat")
176        .join("structs")
177        .join(cached_file_name.clone());
178
179    if !src_dir.exists() {
180        std::fs::create_dir_all(src_dir.clone())?;
181    }
182
183    let src = src_dir.join(format!("{}-{}", src_crate::<C>(), duat_name::<C>()));
184
185    std::fs::OpenOptions::new()
186        .create(true)
187        .read(true)
188        .write(true)
189        .truncate(truncate)
190        .open(src)
191}