paths.rs

  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}