1//! Paths to locations used by Zed.
2
3use std::env;
4use std::path::{Path, PathBuf};
5use std::sync::{LazyLock, OnceLock};
6
7use util::paths::SanitizedPath;
8pub use util::paths::home_dir;
9use util::rel_path::RelPath;
10
11/// A default editorconfig file name to use when resolving project settings.
12pub const EDITORCONFIG_NAME: &str = ".editorconfig";
13
14/// A custom data directory override, set only by `set_custom_data_dir`.
15/// This is used to override the default data directory location.
16/// The directory will be created if it doesn't exist when set.
17static CUSTOM_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
18
19/// The resolved data directory, combining custom override or platform defaults.
20/// This is set once and cached for subsequent calls.
21/// On macOS, this is `~/Library/Application Support/Zed`.
22/// On Linux/FreeBSD, this is `$XDG_DATA_HOME/zed`.
23/// On Windows, this is `%LOCALAPPDATA%\Zed`.
24static CURRENT_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
25
26/// The resolved config directory, combining custom override or platform defaults.
27/// This is set once and cached for subsequent calls.
28/// On macOS, this is `~/.config/zed`.
29/// On Linux/FreeBSD, this is `$XDG_CONFIG_HOME/zed`.
30/// On Windows, this is `%APPDATA%\Zed`.
31static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
32
33/// Returns the relative path to the zed_server directory on the ssh host.
34pub fn remote_server_dir_relative() -> &'static RelPath {
35 static CACHED: LazyLock<&'static RelPath> =
36 LazyLock::new(|| RelPath::unix(".zed_server").unwrap());
37 *CACHED
38}
39
40// Remove this once 223 goes stable
41/// Returns the relative path to the zed_wsl_server directory on the wsl host.
42pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
43 static CACHED: LazyLock<&'static RelPath> =
44 LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap());
45 *CACHED
46}
47
48/// Sets a custom directory for all user data, overriding the default data directory.
49/// This function must be called before any other path operations that depend on the data directory.
50/// The directory's path will be canonicalized to an absolute path by a blocking FS operation.
51/// The directory will be created if it doesn't exist.
52///
53/// # Arguments
54///
55/// * `dir` - The path to use as the custom data directory. This will be used as the base
56/// directory for all user data, including databases, extensions, and logs.
57///
58/// # Returns
59///
60/// A reference to the static `PathBuf` containing the custom data directory path.
61///
62/// # Panics
63///
64/// Panics if:
65/// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`)
66/// * The directory's path cannot be canonicalized to an absolute path
67/// * The directory cannot be created
68pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf {
69 if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() {
70 panic!("set_custom_data_dir called after data_dir or config_dir was initialized");
71 }
72 CUSTOM_DATA_DIR.get_or_init(|| {
73 let path = PathBuf::from(dir);
74 std::fs::create_dir_all(&path).expect("failed to create custom data directory");
75 let canonicalized = path
76 .canonicalize()
77 .expect("failed to canonicalize custom data directory's path to an absolute path");
78 SanitizedPath::new(&canonicalized).as_path().to_path_buf()
79 })
80}
81
82/// Returns the path to the configuration directory used by Zed.
83pub fn config_dir() -> &'static PathBuf {
84 CONFIG_DIR.get_or_init(|| {
85 if let Some(custom_dir) = CUSTOM_DATA_DIR.get() {
86 custom_dir.join("config")
87 } else if cfg!(target_os = "windows") {
88 dirs::config_dir()
89 .expect("failed to determine RoamingAppData directory")
90 .join("Zed")
91 } else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
92 if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") {
93 flatpak_xdg_config.into()
94 } else {
95 dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory")
96 }
97 .join("zed")
98 } else {
99 home_dir().join(".config").join("zed")
100 }
101 })
102}
103
104/// Returns the path to the data directory used by Zed.
105pub fn data_dir() -> &'static PathBuf {
106 CURRENT_DATA_DIR.get_or_init(|| {
107 if let Some(custom_dir) = CUSTOM_DATA_DIR.get() {
108 custom_dir.clone()
109 } else if cfg!(target_os = "macos") {
110 home_dir().join("Library/Application Support/Zed")
111 } else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
112 if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") {
113 flatpak_xdg_data.into()
114 } else {
115 dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory")
116 }
117 .join("zed")
118 } else if cfg!(target_os = "windows") {
119 dirs::data_local_dir()
120 .expect("failed to determine LocalAppData directory")
121 .join("Zed")
122 } else {
123 config_dir().clone() // Fallback
124 }
125 })
126}
127
128pub fn state_dir() -> &'static PathBuf {
129 static STATE_DIR: OnceLock<PathBuf> = OnceLock::new();
130 STATE_DIR.get_or_init(|| {
131 if cfg!(target_os = "macos") {
132 return home_dir().join(".local").join("state").join("Zed");
133 }
134
135 if cfg!(any(target_os = "linux", target_os = "freebsd")) {
136 return if let Ok(flatpak_xdg_state) = std::env::var("FLATPAK_XDG_STATE_HOME") {
137 flatpak_xdg_state.into()
138 } else {
139 dirs::state_dir().expect("failed to determine XDG_STATE_HOME directory")
140 }
141 .join("zed");
142 } else {
143 // Windows
144 return dirs::data_local_dir()
145 .expect("failed to determine LocalAppData directory")
146 .join("Zed");
147 }
148 })
149}
150
151/// Returns the path to the temp directory used by Zed.
152pub fn temp_dir() -> &'static PathBuf {
153 static TEMP_DIR: OnceLock<PathBuf> = OnceLock::new();
154 TEMP_DIR.get_or_init(|| {
155 if cfg!(target_os = "macos") {
156 return dirs::cache_dir()
157 .expect("failed to determine cachesDirectory directory")
158 .join("Zed");
159 }
160
161 if cfg!(target_os = "windows") {
162 return dirs::cache_dir()
163 .expect("failed to determine LocalAppData directory")
164 .join("Zed");
165 }
166
167 if cfg!(any(target_os = "linux", target_os = "freebsd")) {
168 return if let Ok(flatpak_xdg_cache) = std::env::var("FLATPAK_XDG_CACHE_HOME") {
169 flatpak_xdg_cache.into()
170 } else {
171 dirs::cache_dir().expect("failed to determine XDG_CACHE_HOME directory")
172 }
173 .join("zed");
174 }
175
176 home_dir().join(".cache").join("zed")
177 })
178}
179
180/// Returns the path to the hang traces directory.
181pub fn hang_traces_dir() -> &'static PathBuf {
182 static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
183 LOGS_DIR.get_or_init(|| data_dir().join("hang_traces"))
184}
185
186/// Returns the path to the logs directory.
187pub fn logs_dir() -> &'static PathBuf {
188 static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
189 LOGS_DIR.get_or_init(|| {
190 if cfg!(target_os = "macos") {
191 home_dir().join("Library/Logs/Zed")
192 } else {
193 data_dir().join("logs")
194 }
195 })
196}
197
198/// Returns the path to the Zed server directory on this SSH host.
199pub fn remote_server_state_dir() -> &'static PathBuf {
200 static REMOTE_SERVER_STATE: OnceLock<PathBuf> = OnceLock::new();
201 REMOTE_SERVER_STATE.get_or_init(|| data_dir().join("server_state"))
202}
203
204/// Returns the path to the `Zed.log` file.
205pub fn log_file() -> &'static PathBuf {
206 static LOG_FILE: OnceLock<PathBuf> = OnceLock::new();
207 LOG_FILE.get_or_init(|| logs_dir().join("Zed.log"))
208}
209
210/// Returns the path to the `Zed.log.old` file.
211pub fn old_log_file() -> &'static PathBuf {
212 static OLD_LOG_FILE: OnceLock<PathBuf> = OnceLock::new();
213 OLD_LOG_FILE.get_or_init(|| logs_dir().join("Zed.log.old"))
214}
215
216/// Returns the path to the database directory.
217pub fn database_dir() -> &'static PathBuf {
218 static DATABASE_DIR: OnceLock<PathBuf> = OnceLock::new();
219 DATABASE_DIR.get_or_init(|| data_dir().join("db"))
220}
221
222/// Returns the path to the crashes directory, if it exists for the current platform.
223pub fn crashes_dir() -> &'static Option<PathBuf> {
224 static CRASHES_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
225 CRASHES_DIR.get_or_init(|| {
226 cfg!(target_os = "macos").then_some(home_dir().join("Library/Logs/DiagnosticReports"))
227 })
228}
229
230/// Returns the path to the retired crashes directory, if it exists for the current platform.
231pub fn crashes_retired_dir() -> &'static Option<PathBuf> {
232 static CRASHES_RETIRED_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
233 CRASHES_RETIRED_DIR.get_or_init(|| crashes_dir().as_ref().map(|dir| dir.join("Retired")))
234}
235
236/// Returns the path to the `settings.json` file.
237pub fn settings_file() -> &'static PathBuf {
238 static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
239 SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json"))
240}
241
242/// Returns the path to the global settings file.
243pub fn global_settings_file() -> &'static PathBuf {
244 static GLOBAL_SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
245 GLOBAL_SETTINGS_FILE.get_or_init(|| config_dir().join("global_settings.json"))
246}
247
248/// Returns the path to the `settings_backup.json` file.
249pub fn settings_backup_file() -> &'static PathBuf {
250 static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
251 SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json"))
252}
253
254/// Returns the path to the `keymap.json` file.
255pub fn keymap_file() -> &'static PathBuf {
256 static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
257 KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json"))
258}
259
260/// Returns the path to the `keymap_backup.json` file.
261pub fn keymap_backup_file() -> &'static PathBuf {
262 static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
263 KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json"))
264}
265
266/// Returns the path to the `tasks.json` file.
267pub fn tasks_file() -> &'static PathBuf {
268 static TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();
269 TASKS_FILE.get_or_init(|| config_dir().join("tasks.json"))
270}
271
272/// Returns the path to the `debug.json` file.
273pub fn debug_scenarios_file() -> &'static PathBuf {
274 static DEBUG_SCENARIOS_FILE: OnceLock<PathBuf> = OnceLock::new();
275 DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json"))
276}
277
278/// Returns the path to the extensions directory.
279///
280/// This is where installed extensions are stored.
281pub fn extensions_dir() -> &'static PathBuf {
282 static EXTENSIONS_DIR: OnceLock<PathBuf> = OnceLock::new();
283 EXTENSIONS_DIR.get_or_init(|| data_dir().join("extensions"))
284}
285
286/// Returns the path to the extensions directory.
287///
288/// This is where installed extensions are stored on a remote.
289pub fn remote_extensions_dir() -> &'static PathBuf {
290 static EXTENSIONS_DIR: OnceLock<PathBuf> = OnceLock::new();
291 EXTENSIONS_DIR.get_or_init(|| data_dir().join("remote_extensions"))
292}
293
294/// Returns the path to the extensions directory.
295///
296/// This is where installed extensions are stored on a remote.
297pub fn remote_extensions_uploads_dir() -> &'static PathBuf {
298 static UPLOAD_DIR: OnceLock<PathBuf> = OnceLock::new();
299 UPLOAD_DIR.get_or_init(|| remote_extensions_dir().join("uploads"))
300}
301
302/// Returns the path to the themes directory.
303///
304/// This is where themes that are not provided by extensions are stored.
305pub fn themes_dir() -> &'static PathBuf {
306 static THEMES_DIR: OnceLock<PathBuf> = OnceLock::new();
307 THEMES_DIR.get_or_init(|| config_dir().join("themes"))
308}
309
310/// Returns the path to the snippets directory.
311pub fn snippets_dir() -> &'static PathBuf {
312 static SNIPPETS_DIR: OnceLock<PathBuf> = OnceLock::new();
313 SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets"))
314}
315
316// Returns old path to contexts directory.
317// Fallback
318fn text_threads_dir_fallback() -> &'static PathBuf {
319 static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
320 CONTEXTS_DIR.get_or_init(|| {
321 if cfg!(target_os = "macos") {
322 config_dir().join("conversations")
323 } else {
324 data_dir().join("conversations")
325 }
326 })
327}
328/// Returns the path to the contexts directory.
329///
330/// This is where the saved contexts from the Assistant are stored.
331pub fn text_threads_dir() -> &'static PathBuf {
332 let fallback_dir = text_threads_dir_fallback();
333 if fallback_dir.exists() {
334 return fallback_dir;
335 }
336 static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
337 CONTEXTS_DIR.get_or_init(|| state_dir().join("conversations"))
338}
339
340/// Returns the path to the contexts directory.
341///
342/// This is where the prompts for use with the Assistant are stored.
343pub fn prompts_dir() -> &'static PathBuf {
344 static PROMPTS_DIR: OnceLock<PathBuf> = OnceLock::new();
345 PROMPTS_DIR.get_or_init(|| {
346 if cfg!(target_os = "macos") {
347 config_dir().join("prompts")
348 } else {
349 data_dir().join("prompts")
350 }
351 })
352}
353
354/// Returns the path to the prompt templates directory.
355///
356/// This is where the prompt templates for core features can be overridden with templates.
357///
358/// # Arguments
359///
360/// * `dev_mode` - If true, assumes the current working directory is the Zed repository.
361pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf {
362 if let Some(path) = repo_path {
363 let dev_path = path.join("assets").join("prompts");
364 if dev_path.exists() {
365 return dev_path;
366 }
367 }
368
369 static PROMPT_TEMPLATES_DIR: OnceLock<PathBuf> = OnceLock::new();
370 PROMPT_TEMPLATES_DIR
371 .get_or_init(|| {
372 if cfg!(target_os = "macos") {
373 config_dir().join("prompt_overrides")
374 } else {
375 data_dir().join("prompt_overrides")
376 }
377 })
378 .clone()
379}
380
381/// Returns the path to the semantic search's embeddings directory.
382///
383/// This is where the embeddings used to power semantic search are stored.
384pub fn embeddings_dir() -> &'static PathBuf {
385 static EMBEDDINGS_DIR: OnceLock<PathBuf> = OnceLock::new();
386 EMBEDDINGS_DIR.get_or_init(|| {
387 if cfg!(target_os = "macos") {
388 config_dir().join("embeddings")
389 } else {
390 data_dir().join("embeddings")
391 }
392 })
393}
394
395/// Returns the path to the languages directory.
396///
397/// This is where language servers are downloaded to for languages built-in to Zed.
398pub fn languages_dir() -> &'static PathBuf {
399 static LANGUAGES_DIR: OnceLock<PathBuf> = OnceLock::new();
400 LANGUAGES_DIR.get_or_init(|| data_dir().join("languages"))
401}
402
403/// Returns the path to the debug adapters directory
404///
405/// This is where debug adapters are downloaded to for DAPs that are built-in to Zed.
406pub fn debug_adapters_dir() -> &'static PathBuf {
407 static DEBUG_ADAPTERS_DIR: OnceLock<PathBuf> = OnceLock::new();
408 DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters"))
409}
410
411/// Returns the path to the external agents directory
412///
413/// This is where agent servers are downloaded to
414pub fn external_agents_dir() -> &'static PathBuf {
415 static EXTERNAL_AGENTS_DIR: OnceLock<PathBuf> = OnceLock::new();
416 EXTERNAL_AGENTS_DIR.get_or_init(|| data_dir().join("external_agents"))
417}
418
419/// Returns the path to the Copilot directory.
420pub fn copilot_dir() -> &'static PathBuf {
421 static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();
422 COPILOT_DIR.get_or_init(|| data_dir().join("copilot"))
423}
424
425/// Returns the path to the default Prettier directory.
426pub fn default_prettier_dir() -> &'static PathBuf {
427 static DEFAULT_PRETTIER_DIR: OnceLock<PathBuf> = OnceLock::new();
428 DEFAULT_PRETTIER_DIR.get_or_init(|| data_dir().join("prettier"))
429}
430
431/// Returns the path to the remote server binaries directory.
432pub fn remote_servers_dir() -> &'static PathBuf {
433 static REMOTE_SERVERS_DIR: OnceLock<PathBuf> = OnceLock::new();
434 REMOTE_SERVERS_DIR.get_or_init(|| data_dir().join("remote_servers"))
435}
436
437/// Returns the path to the directory where the devcontainer CLI is installed.
438pub fn devcontainer_dir() -> &'static PathBuf {
439 static DEVCONTAINER_DIR: OnceLock<PathBuf> = OnceLock::new();
440 DEVCONTAINER_DIR.get_or_init(|| data_dir().join("devcontainer"))
441}
442
443/// Returns the relative path to a `.zed` folder within a project.
444pub fn local_settings_folder_name() -> &'static str {
445 ".zed"
446}
447
448/// Returns the relative path to a `.vscode` folder within a project.
449pub fn local_vscode_folder_name() -> &'static str {
450 ".vscode"
451}
452
453/// Returns the relative path to a `settings.json` file within a project.
454pub fn local_settings_file_relative_path() -> &'static RelPath {
455 static CACHED: LazyLock<&'static RelPath> =
456 LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap());
457 *CACHED
458}
459
460/// Returns the relative path to a `tasks.json` file within a project.
461pub fn local_tasks_file_relative_path() -> &'static RelPath {
462 static CACHED: LazyLock<&'static RelPath> =
463 LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap());
464 *CACHED
465}
466
467/// Returns the relative path to a `.vscode/tasks.json` file within a project.
468pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
469 static CACHED: LazyLock<&'static RelPath> =
470 LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap());
471 *CACHED
472}
473
474pub fn debug_task_file_name() -> &'static str {
475 "debug.json"
476}
477
478pub fn task_file_name() -> &'static str {
479 "tasks.json"
480}
481
482/// Returns the relative path to a `debug.json` file within a project.
483/// .zed/debug.json
484pub fn local_debug_file_relative_path() -> &'static RelPath {
485 static CACHED: LazyLock<&'static RelPath> =
486 LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap());
487 *CACHED
488}
489
490/// Returns the relative path to a `.vscode/launch.json` file within a project.
491pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
492 static CACHED: LazyLock<&'static RelPath> =
493 LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap());
494 *CACHED
495}
496
497pub fn user_ssh_config_file() -> PathBuf {
498 home_dir().join(".ssh/config")
499}
500
501pub fn global_ssh_config_file() -> Option<&'static Path> {
502 if cfg!(windows) {
503 None
504 } else {
505 Some(Path::new("/etc/ssh/ssh_config"))
506 }
507}
508
509/// Returns candidate paths for the vscode user settings file
510pub fn vscode_settings_file_paths() -> Vec<PathBuf> {
511 let mut paths = vscode_user_data_paths();
512 for path in paths.iter_mut() {
513 path.push("User/settings.json");
514 }
515 paths
516}
517
518/// Returns candidate paths for the cursor user settings file
519pub fn cursor_settings_file_paths() -> Vec<PathBuf> {
520 let mut paths = cursor_user_data_paths();
521 for path in paths.iter_mut() {
522 path.push("User/settings.json");
523 }
524 paths
525}
526
527fn vscode_user_data_paths() -> Vec<PathBuf> {
528 // https://github.com/microsoft/vscode/blob/23e7148cdb6d8a27f0109ff77e5b1e019f8da051/src/vs/platform/environment/node/userDataPath.ts#L45
529 const VSCODE_PRODUCT_NAMES: &[&str] = &[
530 "Code",
531 "Code - Insiders",
532 "Code - OSS",
533 "VSCodium",
534 "VSCodium - Insiders",
535 "Code Dev",
536 "Code - OSS Dev",
537 "code-oss-dev",
538 ];
539 let mut paths = Vec::new();
540 if let Ok(portable_path) = env::var("VSCODE_PORTABLE") {
541 paths.push(Path::new(&portable_path).join("user-data"));
542 }
543 if let Ok(vscode_appdata) = env::var("VSCODE_APPDATA") {
544 for product_name in VSCODE_PRODUCT_NAMES {
545 paths.push(Path::new(&vscode_appdata).join(product_name));
546 }
547 }
548 for product_name in VSCODE_PRODUCT_NAMES {
549 add_vscode_user_data_paths(&mut paths, product_name);
550 }
551 paths
552}
553
554fn cursor_user_data_paths() -> Vec<PathBuf> {
555 let mut paths = Vec::new();
556 add_vscode_user_data_paths(&mut paths, "Cursor");
557 paths
558}
559
560fn add_vscode_user_data_paths(paths: &mut Vec<PathBuf>, product_name: &str) {
561 if cfg!(target_os = "macos") {
562 paths.push(
563 home_dir()
564 .join("Library/Application Support")
565 .join(product_name),
566 );
567 } else if cfg!(target_os = "windows") {
568 if let Some(data_local_dir) = dirs::data_local_dir() {
569 paths.push(data_local_dir.join(product_name));
570 }
571 if let Some(data_dir) = dirs::data_dir() {
572 paths.push(data_dir.join(product_name));
573 }
574 } else {
575 paths.push(
576 dirs::config_dir()
577 .unwrap_or(home_dir().join(".config"))
578 .join(product_name),
579 );
580 }
581}
582
583#[cfg(any(test, feature = "test-support"))]
584pub fn global_gitignore_path() -> Option<PathBuf> {
585 Some(home_dir().join(".config").join("git").join("ignore"))
586}
587
588#[cfg(not(any(test, feature = "test-support")))]
589pub fn global_gitignore_path() -> Option<PathBuf> {
590 static GLOBAL_GITIGNORE_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
591 GLOBAL_GITIGNORE_PATH
592 .get_or_init(::ignore::gitignore::gitconfig_excludes_path)
593 .clone()
594}