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