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}