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 let maybe_col_str =
143 if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
144 &maybe_col_str[..maybe_col_str.len() - 1]
145 } else {
146 maybe_col_str
147 };
148 match maybe_col_str.parse::<u32>() {
149 Ok(col) => Ok(Self {
150 path_like: parse_path_like_str(path_like_str)?,
151 row: Some(row),
152 column: Some(col),
153 }),
154 Err(_) => fallback(s),
155 }
156 }
157 }
158 Err(_) => fallback(s),
159 }
160 }
161 }
162 None => fallback(s),
163 }
164 }
165
166 pub fn map_path_like<P2, E>(
167 self,
168 mapping: impl FnOnce(P) -> Result<P2, E>,
169 ) -> Result<PathLikeWithPosition<P2>, E> {
170 Ok(PathLikeWithPosition {
171 path_like: mapping(self.path_like)?,
172 row: self.row,
173 column: self.column,
174 })
175 }
176
177 pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String {
178 let path_like_string = path_like_to_string(&self.path_like);
179 if let Some(row) = self.row {
180 if let Some(column) = self.column {
181 format!("{path_like_string}:{row}:{column}")
182 } else {
183 format!("{path_like_string}:{row}")
184 }
185 } else {
186 path_like_string
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 type TestPath = PathLikeWithPosition<String>;
196
197 fn parse_str(s: &str) -> TestPath {
198 TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string()))
199 .expect("infallible")
200 }
201
202 #[test]
203 fn path_with_position_parsing_positive() {
204 let input_and_expected = [
205 (
206 "test_file.rs",
207 PathLikeWithPosition {
208 path_like: "test_file.rs".to_string(),
209 row: None,
210 column: None,
211 },
212 ),
213 (
214 "test_file.rs:1",
215 PathLikeWithPosition {
216 path_like: "test_file.rs".to_string(),
217 row: Some(1),
218 column: None,
219 },
220 ),
221 (
222 "test_file.rs:1:2",
223 PathLikeWithPosition {
224 path_like: "test_file.rs".to_string(),
225 row: Some(1),
226 column: Some(2),
227 },
228 ),
229 ];
230
231 for (input, expected) in input_and_expected {
232 let actual = parse_str(input);
233 assert_eq!(
234 actual, expected,
235 "For positive case input str '{input}', got a parse mismatch"
236 );
237 }
238 }
239
240 #[test]
241 fn path_with_position_parsing_negative() {
242 for input in [
243 "test_file.rs:a",
244 "test_file.rs:a:b",
245 "test_file.rs::",
246 "test_file.rs::1",
247 "test_file.rs:1::",
248 "test_file.rs::1:2",
249 "test_file.rs:1::2",
250 "test_file.rs:1:2:3",
251 ] {
252 let actual = parse_str(input);
253 assert_eq!(
254 actual,
255 PathLikeWithPosition {
256 path_like: input.to_string(),
257 row: None,
258 column: None,
259 },
260 "For negative case input str '{input}', got a parse mismatch"
261 );
262 }
263 }
264
265 // Trim off trailing `:`s for otherwise valid input.
266 #[test]
267 fn path_with_position_parsing_special() {
268 let input_and_expected = [
269 (
270 "test_file.rs:",
271 PathLikeWithPosition {
272 path_like: "test_file.rs".to_string(),
273 row: None,
274 column: None,
275 },
276 ),
277 (
278 "test_file.rs:1:",
279 PathLikeWithPosition {
280 path_like: "test_file.rs".to_string(),
281 row: Some(1),
282 column: None,
283 },
284 ),
285 (
286 "crates/file_finder/src/file_finder.rs:1902:13:",
287 PathLikeWithPosition {
288 path_like: "crates/file_finder/src/file_finder.rs".to_string(),
289 row: Some(1902),
290 column: Some(13),
291 },
292 ),
293 ];
294
295 for (input, expected) in input_and_expected {
296 let actual = parse_str(input);
297 assert_eq!(
298 actual, expected,
299 "For special case input str '{input}', got a parse mismatch"
300 );
301 }
302 }
303
304 #[test]
305 fn test_path_compact() {
306 let path: PathBuf = [
307 HOME.to_string_lossy().to_string(),
308 "some_file.txt".to_string(),
309 ]
310 .iter()
311 .collect();
312 if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
313 assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
314 } else {
315 assert_eq!(path.compact().to_str(), path.to_str());
316 }
317 }
318
319 #[test]
320 fn test_icon_suffix() {
321 // No dots in name
322 let path = Path::new("/a/b/c/file_name.rs");
323 assert_eq!(path.icon_suffix(), Some("rs"));
324
325 // Single dot in name
326 let path = Path::new("/a/b/c/file.name.rs");
327 assert_eq!(path.icon_suffix(), Some("rs"));
328
329 // Multiple dots in name
330 let path = Path::new("/a/b/c/long.file.name.rs");
331 assert_eq!(path.icon_suffix(), Some("rs"));
332
333 // Hidden file, no extension
334 let path = Path::new("/a/b/c/.gitignore");
335 assert_eq!(path.icon_suffix(), Some("gitignore"));
336
337 // Hidden file, with extension
338 let path = Path::new("/a/b/c/.eslintrc.js");
339 assert_eq!(path.icon_suffix(), Some("eslintrc.js"));
340 }
341
342 #[test]
343 fn test_extension_or_hidden_file_name() {
344 // No dots in name
345 let path = Path::new("/a/b/c/file_name.rs");
346 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
347
348 // Single dot in name
349 let path = Path::new("/a/b/c/file.name.rs");
350 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
351
352 // Multiple dots in name
353 let path = Path::new("/a/b/c/long.file.name.rs");
354 assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
355
356 // Hidden file, no extension
357 let path = Path::new("/a/b/c/.gitignore");
358 assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
359
360 // Hidden file, with extension
361 let path = Path::new("/a/b/c/.eslintrc.js");
362 assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
363 }
364}