paths.rs

  1use std::path::{Path, PathBuf};
  2
  3use serde::{Deserialize, Serialize};
  4
  5lazy_static::lazy_static! {
  6    pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
  7    pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
  8    pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
  9    pub static ref EMBEDDINGS_DIR: PathBuf = HOME.join(".config/zed/embeddings");
 10    pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
 11    pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
 12    pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
 13    pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot");
 14    pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db");
 15    pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
 16    pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
 17    pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
 18    pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
 19    pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
 20    pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
 21}
 22
 23pub mod legacy {
 24    use std::path::PathBuf;
 25
 26    lazy_static::lazy_static! {
 27        static ref CONFIG_DIR: PathBuf = super::HOME.join(".zed");
 28        pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
 29        pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
 30    }
 31}
 32
 33pub trait PathExt {
 34    fn compact(&self) -> PathBuf;
 35    fn icon_suffix(&self) -> Option<&str>;
 36    fn extension_or_hidden_file_name(&self) -> Option<&str>;
 37}
 38
 39impl<T: AsRef<Path>> PathExt for T {
 40    /// Compacts a given file path by replacing the user's home directory
 41    /// prefix with a tilde (`~`).
 42    ///
 43    /// # Returns
 44    ///
 45    /// * A `PathBuf` containing the compacted file path. If the input path
 46    ///   does not have the user's home directory prefix, or if we are not on
 47    ///   Linux or macOS, the original path is returned unchanged.
 48    fn compact(&self) -> PathBuf {
 49        if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
 50            match self.as_ref().strip_prefix(HOME.as_path()) {
 51                Ok(relative_path) => {
 52                    let mut shortened_path = PathBuf::new();
 53                    shortened_path.push("~");
 54                    shortened_path.push(relative_path);
 55                    shortened_path
 56                }
 57                Err(_) => self.as_ref().to_path_buf(),
 58            }
 59        } else {
 60            self.as_ref().to_path_buf()
 61        }
 62    }
 63
 64    /// Returns a suffix of the path that is used to determine which file icon to use
 65    fn icon_suffix(&self) -> Option<&str> {
 66        let file_name = self.as_ref().file_name()?.to_str()?;
 67
 68        if file_name.starts_with(".") {
 69            return file_name.strip_prefix(".");
 70        }
 71
 72        self.as_ref()
 73            .extension()
 74            .and_then(|extension| extension.to_str())
 75    }
 76
 77    /// Returns a file's extension or, if the file is hidden, its name without the leading dot
 78    fn extension_or_hidden_file_name(&self) -> Option<&str> {
 79        if let Some(extension) = self.as_ref().extension() {
 80            return extension.to_str();
 81        }
 82
 83        self.as_ref().file_name()?.to_str()?.split('.').last()
 84    }
 85}
 86
 87/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
 88pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 89
 90/// A representation of a path-like string with optional row and column numbers.
 91/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc.
 92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 93pub struct PathLikeWithPosition<P> {
 94    pub path_like: P,
 95    pub row: Option<u32>,
 96    // Absent if row is absent.
 97    pub column: Option<u32>,
 98}
 99
100impl<P> PathLikeWithPosition<P> {
101    /// Parses a string that possibly has `:row:column` suffix.
102    /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
103    /// If any of the row/column component parsing fails, the whole string is then parsed as a path like.
104    pub fn parse_str<E>(
105        s: &str,
106        parse_path_like_str: impl Fn(&str) -> Result<P, E>,
107    ) -> Result<Self, E> {
108        let fallback = |fallback_str| {
109            Ok(Self {
110                path_like: parse_path_like_str(fallback_str)?,
111                row: None,
112                column: None,
113            })
114        };
115
116        match s.trim().split_once(FILE_ROW_COLUMN_DELIMITER) {
117            Some((path_like_str, maybe_row_and_col_str)) => {
118                let path_like_str = path_like_str.trim();
119                let maybe_row_and_col_str = maybe_row_and_col_str.trim();
120                if path_like_str.is_empty() {
121                    fallback(s)
122                } else if maybe_row_and_col_str.is_empty() {
123                    fallback(path_like_str)
124                } else {
125                    let (row_parse_result, maybe_col_str) =
126                        match maybe_row_and_col_str.split_once(FILE_ROW_COLUMN_DELIMITER) {
127                            Some((maybe_row_str, maybe_col_str)) => {
128                                (maybe_row_str.parse::<u32>(), maybe_col_str.trim())
129                            }
130                            None => (maybe_row_and_col_str.parse::<u32>(), ""),
131                        };
132
133                    match row_parse_result {
134                        Ok(row) => {
135                            if maybe_col_str.is_empty() {
136                                Ok(Self {
137                                    path_like: parse_path_like_str(path_like_str)?,
138                                    row: Some(row),
139                                    column: None,
140                                })
141                            } else {
142                                match maybe_col_str.parse::<u32>() {
143                                    Ok(col) => Ok(Self {
144                                        path_like: parse_path_like_str(path_like_str)?,
145                                        row: Some(row),
146                                        column: Some(col),
147                                    }),
148                                    Err(_) => fallback(s),
149                                }
150                            }
151                        }
152                        Err(_) => fallback(s),
153                    }
154                }
155            }
156            None => fallback(s),
157        }
158    }
159
160    pub fn map_path_like<P2, E>(
161        self,
162        mapping: impl FnOnce(P) -> Result<P2, E>,
163    ) -> Result<PathLikeWithPosition<P2>, E> {
164        Ok(PathLikeWithPosition {
165            path_like: mapping(self.path_like)?,
166            row: self.row,
167            column: self.column,
168        })
169    }
170
171    pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String {
172        let path_like_string = path_like_to_string(&self.path_like);
173        if let Some(row) = self.row {
174            if let Some(column) = self.column {
175                format!("{path_like_string}:{row}:{column}")
176            } else {
177                format!("{path_like_string}:{row}")
178            }
179        } else {
180            path_like_string
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    type TestPath = PathLikeWithPosition<String>;
190
191    fn parse_str(s: &str) -> TestPath {
192        TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string()))
193            .expect("infallible")
194    }
195
196    #[test]
197    fn path_with_position_parsing_positive() {
198        let input_and_expected = [
199            (
200                "test_file.rs",
201                PathLikeWithPosition {
202                    path_like: "test_file.rs".to_string(),
203                    row: None,
204                    column: None,
205                },
206            ),
207            (
208                "test_file.rs:1",
209                PathLikeWithPosition {
210                    path_like: "test_file.rs".to_string(),
211                    row: Some(1),
212                    column: None,
213                },
214            ),
215            (
216                "test_file.rs:1:2",
217                PathLikeWithPosition {
218                    path_like: "test_file.rs".to_string(),
219                    row: Some(1),
220                    column: Some(2),
221                },
222            ),
223        ];
224
225        for (input, expected) in input_and_expected {
226            let actual = parse_str(input);
227            assert_eq!(
228                actual, expected,
229                "For positive case input str '{input}', got a parse mismatch"
230            );
231        }
232    }
233
234    #[test]
235    fn path_with_position_parsing_negative() {
236        for input in [
237            "test_file.rs:a",
238            "test_file.rs:a:b",
239            "test_file.rs::",
240            "test_file.rs::1",
241            "test_file.rs:1::",
242            "test_file.rs::1:2",
243            "test_file.rs:1::2",
244            "test_file.rs:1:2:",
245            "test_file.rs:1:2:3",
246        ] {
247            let actual = parse_str(input);
248            assert_eq!(
249                actual,
250                PathLikeWithPosition {
251                    path_like: input.to_string(),
252                    row: None,
253                    column: None,
254                },
255                "For negative case input str '{input}', got a parse mismatch"
256            );
257        }
258    }
259
260    // Trim off trailing `:`s for otherwise valid input.
261    #[test]
262    fn path_with_position_parsing_special() {
263        let input_and_expected = [
264            (
265                "test_file.rs:",
266                PathLikeWithPosition {
267                    path_like: "test_file.rs".to_string(),
268                    row: None,
269                    column: None,
270                },
271            ),
272            (
273                "test_file.rs:1:",
274                PathLikeWithPosition {
275                    path_like: "test_file.rs".to_string(),
276                    row: Some(1),
277                    column: None,
278                },
279            ),
280        ];
281
282        for (input, expected) in input_and_expected {
283            let actual = parse_str(input);
284            assert_eq!(
285                actual, expected,
286                "For special case input str '{input}', got a parse mismatch"
287            );
288        }
289    }
290
291    #[test]
292    fn test_path_compact() {
293        let path: PathBuf = [
294            HOME.to_string_lossy().to_string(),
295            "some_file.txt".to_string(),
296        ]
297        .iter()
298        .collect();
299        if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
300            assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
301        } else {
302            assert_eq!(path.compact().to_str(), path.to_str());
303        }
304    }
305
306    #[test]
307    fn test_icon_suffix() {
308        // No dots in name
309        let path = Path::new("/a/b/c/file_name.rs");
310        assert_eq!(path.icon_suffix(), Some("rs"));
311
312        // Single dot in name
313        let path = Path::new("/a/b/c/file.name.rs");
314        assert_eq!(path.icon_suffix(), Some("rs"));
315
316        // Multiple dots in name
317        let path = Path::new("/a/b/c/long.file.name.rs");
318        assert_eq!(path.icon_suffix(), Some("rs"));
319
320        // Hidden file, no extension
321        let path = Path::new("/a/b/c/.gitignore");
322        assert_eq!(path.icon_suffix(), Some("gitignore"));
323
324        // Hidden file, with extension
325        let path = Path::new("/a/b/c/.eslintrc.js");
326        assert_eq!(path.icon_suffix(), Some("eslintrc.js"));
327    }
328
329    #[test]
330    fn test_extension_or_hidden_file_name() {
331        // No dots in name
332        let path = Path::new("/a/b/c/file_name.rs");
333        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
334
335        // Single dot in name
336        let path = Path::new("/a/b/c/file.name.rs");
337        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
338
339        // Multiple dots in name
340        let path = Path::new("/a/b/c/long.file.name.rs");
341        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
342
343        // Hidden file, no extension
344        let path = Path::new("/a/b/c/.gitignore");
345        assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
346
347        // Hidden file, with extension
348        let path = Path::new("/a/b/c/.eslintrc.js");
349        assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
350    }
351}