Derive data directory names from APP_NAME constant (#55805)

Richard Feldman created

Replace all hardcoded `"Zed"` and `"zed"` directory names in
`config_dir()`, `data_dir()`, `state_dir()`, and `temp_dir()` with a
single `APP_NAME` constant in the `paths` crate.

- On macOS/Windows (native paths like Application Support, LocalAppData,
Caches), `APP_NAME` is used directly (`"Zed"`).
- On Linux/FreeBSD (XDG-style paths), `app_name_lowercase()` is used
(`"zed"`).

This ensures forks that change `APP_NAME` automatically get their own
data directories, preventing them from stomping on Zed users' databases
and config.

Release Notes:

- N/A

Change summary

crates/paths/src/paths.rs | 69 +++++++++++++++++++++++++++++++---------
crates/zed/src/main.rs    | 10 +++++
2 files changed, 63 insertions(+), 16 deletions(-)

Detailed changes

crates/paths/src/paths.rs 🔗

@@ -11,6 +11,41 @@ use util::rel_path::RelPath;
 /// A default editorconfig file name to use when resolving project settings.
 pub const EDITORCONFIG_NAME: &str = ".editorconfig";
 
+/// The application name, used to derive platform-specific data, config, cache,
+/// and state directory paths.
+///
+/// Forks should change this to avoid colliding with Zed's user data.
+pub const APP_NAME: &str = "Zed";
+
+/// Lowercased form of [`APP_NAME`], for use in XDG-style paths on
+/// Linux/FreeBSD and the macOS `~/.config` fallback.
+pub const APP_NAME_LOWERCASE: &str = {
+    assert!(!APP_NAME.is_empty(), "APP_NAME must not be empty");
+    assert!(APP_NAME.as_bytes().is_ascii(), "APP_NAME must be ASCII");
+    const BYTES: [u8; APP_NAME.len()] = {
+        let mut bytes = [0u8; APP_NAME.len()];
+        let mut i = 0;
+        while i < APP_NAME.len() {
+            assert!(
+                APP_NAME.as_bytes()[i] != b'/' && APP_NAME.as_bytes()[i] != b'\\',
+                "APP_NAME must not contain path separators",
+            );
+            assert!(
+                APP_NAME.as_bytes()[i] >= 0x20,
+                "APP_NAME must not contain control characters"
+            );
+            bytes[i] = APP_NAME.as_bytes()[i];
+            i += 1;
+        }
+        bytes.make_ascii_lowercase();
+        bytes
+    };
+    match std::str::from_utf8(&BYTES) {
+        Ok(s) => s,
+        Err(_) => unreachable!(),
+    }
+};
+
 /// A custom data directory override, set only by `set_custom_data_dir`.
 /// This is used to override the default data directory location.
 /// The directory will be created if it doesn't exist when set.
@@ -91,16 +126,16 @@ pub fn config_dir() -> &'static PathBuf {
         } else if cfg!(target_os = "windows") {
             dirs::config_dir()
                 .expect("failed to determine RoamingAppData directory")
-                .join("Zed")
+                .join(APP_NAME)
         } else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
             if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") {
                 flatpak_xdg_config.into()
             } else {
                 dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory")
             }
-            .join("zed")
+            .join(APP_NAME_LOWERCASE)
         } else {
-            home_dir().join(".config").join("zed")
+            home_dir().join(".config").join(APP_NAME_LOWERCASE)
         }
     })
 }
@@ -111,18 +146,20 @@ pub fn data_dir() -> &'static PathBuf {
         if let Some(custom_dir) = CUSTOM_DATA_DIR.get() {
             custom_dir.clone()
         } else if cfg!(target_os = "macos") {
-            home_dir().join("Library/Application Support/Zed")
+            home_dir()
+                .join("Library/Application Support")
+                .join(APP_NAME)
         } else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
             if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") {
                 flatpak_xdg_data.into()
             } else {
                 dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory")
             }
-            .join("zed")
+            .join(APP_NAME_LOWERCASE)
         } else if cfg!(target_os = "windows") {
             dirs::data_local_dir()
                 .expect("failed to determine LocalAppData directory")
-                .join("Zed")
+                .join(APP_NAME)
         } else {
             config_dir().clone() // Fallback
         }
@@ -133,7 +170,7 @@ pub fn state_dir() -> &'static PathBuf {
     static STATE_DIR: OnceLock<PathBuf> = OnceLock::new();
     STATE_DIR.get_or_init(|| {
         if cfg!(target_os = "macos") {
-            return home_dir().join(".local").join("state").join("Zed");
+            return home_dir().join(".local").join("state").join(APP_NAME);
         }
 
         if cfg!(any(target_os = "linux", target_os = "freebsd")) {
@@ -142,12 +179,12 @@ pub fn state_dir() -> &'static PathBuf {
             } else {
                 dirs::state_dir().expect("failed to determine XDG_STATE_HOME directory")
             }
-            .join("zed");
+            .join(APP_NAME_LOWERCASE);
         } else {
             // Windows
             return dirs::data_local_dir()
                 .expect("failed to determine LocalAppData directory")
-                .join("Zed");
+                .join(APP_NAME);
         }
     })
 }
@@ -159,13 +196,13 @@ pub fn temp_dir() -> &'static PathBuf {
         if cfg!(target_os = "macos") {
             return dirs::cache_dir()
                 .expect("failed to determine cachesDirectory directory")
-                .join("Zed");
+                .join(APP_NAME);
         }
 
         if cfg!(target_os = "windows") {
             return dirs::cache_dir()
                 .expect("failed to determine LocalAppData directory")
-                .join("Zed");
+                .join(APP_NAME);
         }
 
         if cfg!(any(target_os = "linux", target_os = "freebsd")) {
@@ -174,10 +211,10 @@ pub fn temp_dir() -> &'static PathBuf {
             } else {
                 dirs::cache_dir().expect("failed to determine XDG_CACHE_HOME directory")
             }
-            .join("zed");
+            .join(APP_NAME_LOWERCASE);
         }
 
-        home_dir().join(".cache").join("zed")
+        home_dir().join(".cache").join(APP_NAME_LOWERCASE)
     })
 }
 
@@ -192,7 +229,7 @@ pub fn logs_dir() -> &'static PathBuf {
     static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
     LOGS_DIR.get_or_init(|| {
         if cfg!(target_os = "macos") {
-            home_dir().join("Library/Logs/Zed")
+            home_dir().join("Library/Logs").join(APP_NAME)
         } else {
             data_dir().join("logs")
         }
@@ -208,13 +245,13 @@ pub fn remote_server_state_dir() -> &'static PathBuf {
 /// Returns the path to the `Zed.log` file.
 pub fn log_file() -> &'static PathBuf {
     static LOG_FILE: OnceLock<PathBuf> = OnceLock::new();
-    LOG_FILE.get_or_init(|| logs_dir().join("Zed.log"))
+    LOG_FILE.get_or_init(|| logs_dir().join(format!("{}.log", APP_NAME)))
 }
 
 /// Returns the path to the `Zed.log.old` file.
 pub fn old_log_file() -> &'static PathBuf {
     static OLD_LOG_FILE: OnceLock<PathBuf> = OnceLock::new();
-    OLD_LOG_FILE.get_or_init(|| logs_dir().join("Zed.log.old"))
+    OLD_LOG_FILE.get_or_init(|| logs_dir().join(format!("{}.log.old", APP_NAME)))
 }
 
 /// Returns the path to the database directory.

crates/zed/src/main.rs 🔗

@@ -4,6 +4,16 @@
 mod reliability;
 mod zed;
 
+// Ensure the binary name stays in sync with APP_NAME so that the paths used
+// at runtime (data dir, config dir, etc.) match what the binary is called.
+const _: () = assert!(
+    paths::APP_NAME_LOWERCASE
+        .as_bytes()
+        .eq_ignore_ascii_case(env!("CARGO_BIN_NAME").as_bytes()),
+    "paths::APP_NAME_LOWERCASE must match the binary name. \
+     Forks: update APP_NAME in crates/paths/src/paths.rs when renaming the binary.",
+);
+
 use agent::{SharedThread, ThreadStore};
 use agent_client_protocol::schema as acp;
 use agent_ui::AgentPanel;