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 LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
  9    pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
 10    pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
 11    pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot");
 12    pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db");
 13    pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
 14    pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
 15    pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
 16    pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
 17    pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
 18}
 19
 20pub mod legacy {
 21    use std::path::PathBuf;
 22
 23    lazy_static::lazy_static! {
 24        static ref CONFIG_DIR: PathBuf = super::HOME.join(".zed");
 25        pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
 26        pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
 27    }
 28}
 29
 30/// Compacts a given file path by replacing the user's home directory
 31/// prefix with a tilde (`~`).
 32///
 33/// # Arguments
 34///
 35/// * `path` - A reference to a `Path` representing the file path to compact.
 36///
 37/// # Examples
 38///
 39/// ```
 40/// use std::path::{Path, PathBuf};
 41/// use util::paths::compact;
 42/// let path: PathBuf = [
 43///     util::paths::HOME.to_string_lossy().to_string(),
 44///     "some_file.txt".to_string(),
 45///  ]
 46///  .iter()
 47///  .collect();
 48/// if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
 49///     assert_eq!(compact(&path).to_str(), Some("~/some_file.txt"));
 50/// } else {
 51///     assert_eq!(compact(&path).to_str(), path.to_str());
 52/// }
 53/// ```
 54///
 55/// # Returns
 56///
 57/// * A `PathBuf` containing the compacted file path. If the input path
 58///   does not have the user's home directory prefix, or if we are not on
 59///   Linux or macOS, the original path is returned unchanged.
 60pub fn compact(path: &Path) -> PathBuf {
 61    if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
 62        match path.strip_prefix(HOME.as_path()) {
 63            Ok(relative_path) => {
 64                let mut shortened_path = PathBuf::new();
 65                shortened_path.push("~");
 66                shortened_path.push(relative_path);
 67                shortened_path
 68            }
 69            Err(_) => path.to_path_buf(),
 70        }
 71    } else {
 72        path.to_path_buf()
 73    }
 74}
 75
 76/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
 77pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 78
 79/// A representation of a path-like string with optional row and column numbers.
 80/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc.
 81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 82pub struct PathLikeWithPosition<P> {
 83    pub path_like: P,
 84    pub row: Option<u32>,
 85    // Absent if row is absent.
 86    pub column: Option<u32>,
 87}
 88
 89impl<P> PathLikeWithPosition<P> {
 90    /// Parses a string that possibly has `:row:column` suffix.
 91    /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
 92    /// If any of the row/column component parsing fails, the whole string is then parsed as a path like.
 93    pub fn parse_str<E>(
 94        s: &str,
 95        parse_path_like_str: impl Fn(&str) -> Result<P, E>,
 96    ) -> Result<Self, E> {
 97        let fallback = |fallback_str| {
 98            Ok(Self {
 99                path_like: parse_path_like_str(fallback_str)?,
100                row: None,
101                column: None,
102            })
103        };
104
105        match s.trim().split_once(FILE_ROW_COLUMN_DELIMITER) {
106            Some((path_like_str, maybe_row_and_col_str)) => {
107                let path_like_str = path_like_str.trim();
108                let maybe_row_and_col_str = maybe_row_and_col_str.trim();
109                if path_like_str.is_empty() {
110                    fallback(s)
111                } else if maybe_row_and_col_str.is_empty() {
112                    fallback(path_like_str)
113                } else {
114                    let (row_parse_result, maybe_col_str) =
115                        match maybe_row_and_col_str.split_once(FILE_ROW_COLUMN_DELIMITER) {
116                            Some((maybe_row_str, maybe_col_str)) => {
117                                (maybe_row_str.parse::<u32>(), maybe_col_str.trim())
118                            }
119                            None => (maybe_row_and_col_str.parse::<u32>(), ""),
120                        };
121
122                    match row_parse_result {
123                        Ok(row) => {
124                            if maybe_col_str.is_empty() {
125                                Ok(Self {
126                                    path_like: parse_path_like_str(path_like_str)?,
127                                    row: Some(row),
128                                    column: None,
129                                })
130                            } else {
131                                match maybe_col_str.parse::<u32>() {
132                                    Ok(col) => Ok(Self {
133                                        path_like: parse_path_like_str(path_like_str)?,
134                                        row: Some(row),
135                                        column: Some(col),
136                                    }),
137                                    Err(_) => fallback(s),
138                                }
139                            }
140                        }
141                        Err(_) => fallback(s),
142                    }
143                }
144            }
145            None => fallback(s),
146        }
147    }
148
149    pub fn map_path_like<P2, E>(
150        self,
151        mapping: impl FnOnce(P) -> Result<P2, E>,
152    ) -> Result<PathLikeWithPosition<P2>, E> {
153        Ok(PathLikeWithPosition {
154            path_like: mapping(self.path_like)?,
155            row: self.row,
156            column: self.column,
157        })
158    }
159
160    pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String {
161        let path_like_string = path_like_to_string(&self.path_like);
162        if let Some(row) = self.row {
163            if let Some(column) = self.column {
164                format!("{path_like_string}:{row}:{column}")
165            } else {
166                format!("{path_like_string}:{row}")
167            }
168        } else {
169            path_like_string
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    type TestPath = PathLikeWithPosition<String>;
179
180    fn parse_str(s: &str) -> TestPath {
181        TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string()))
182            .expect("infallible")
183    }
184
185    #[test]
186    fn path_with_position_parsing_positive() {
187        let input_and_expected = [
188            (
189                "test_file.rs",
190                PathLikeWithPosition {
191                    path_like: "test_file.rs".to_string(),
192                    row: None,
193                    column: None,
194                },
195            ),
196            (
197                "test_file.rs:1",
198                PathLikeWithPosition {
199                    path_like: "test_file.rs".to_string(),
200                    row: Some(1),
201                    column: None,
202                },
203            ),
204            (
205                "test_file.rs:1:2",
206                PathLikeWithPosition {
207                    path_like: "test_file.rs".to_string(),
208                    row: Some(1),
209                    column: Some(2),
210                },
211            ),
212        ];
213
214        for (input, expected) in input_and_expected {
215            let actual = parse_str(input);
216            assert_eq!(
217                actual, expected,
218                "For positive case input str '{input}', got a parse mismatch"
219            );
220        }
221    }
222
223    #[test]
224    fn path_with_position_parsing_negative() {
225        for input in [
226            "test_file.rs:a",
227            "test_file.rs:a:b",
228            "test_file.rs::",
229            "test_file.rs::1",
230            "test_file.rs:1::",
231            "test_file.rs::1:2",
232            "test_file.rs:1::2",
233            "test_file.rs:1:2:",
234            "test_file.rs:1:2:3",
235        ] {
236            let actual = parse_str(input);
237            assert_eq!(
238                actual,
239                PathLikeWithPosition {
240                    path_like: input.to_string(),
241                    row: None,
242                    column: None,
243                },
244                "For negative case input str '{input}', got a parse mismatch"
245            );
246        }
247    }
248
249    // Trim off trailing `:`s for otherwise valid input.
250    #[test]
251    fn path_with_position_parsing_special() {
252        let input_and_expected = [
253            (
254                "test_file.rs:",
255                PathLikeWithPosition {
256                    path_like: "test_file.rs".to_string(),
257                    row: None,
258                    column: None,
259                },
260            ),
261            (
262                "test_file.rs:1:",
263                PathLikeWithPosition {
264                    path_like: "test_file.rs".to_string(),
265                    row: Some(1),
266                    column: None,
267                },
268            ),
269        ];
270
271        for (input, expected) in input_and_expected {
272            let actual = parse_str(input);
273            assert_eq!(
274                actual, expected,
275                "For special case input str '{input}', got a parse mismatch"
276            );
277        }
278    }
279}