diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 9d41eb1562943683aae3e785b1daac8bc3bfeb1a..4f2e404b12875ba25f372b138458f264835f2a1b 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -19,6 +19,7 @@ use std::{ }; use util::ResultExt; use util::archive::extract_zip; +use util::paths::{SanitizedPath, SanitizedPathBuf}; const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS"; @@ -371,7 +372,7 @@ trait NodeRuntimeTrait: Send + Sync { #[derive(Clone)] struct ManagedNodeRuntime { - installation_path: PathBuf, + installation_path: SanitizedPathBuf, } impl ManagedNodeRuntime { @@ -483,9 +484,13 @@ impl ManagedNodeRuntime { ArchiveType::TarGz => { let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); let archive = Archive::new(decompressed_bytes); - archive.unpack(&node_containing_dir).await?; + archive + .unpack(node_containing_dir.as_path().as_path()) + .await?; + } + ArchiveType::Zip => { + extract_zip(node_containing_dir.as_path().as_path(), body).await? } - ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?, } log::info!("Extracted Node.js to {}", node_containing_dir.display()) } @@ -501,7 +506,7 @@ impl ManagedNodeRuntime { } } -fn path_with_node_binary_prepended(node_binary: &Path) -> Option { +fn path_with_node_binary_prepended(node_binary: &SanitizedPath) -> Option { let existing_path = env::var_os("PATH"); let node_bin_dir = node_binary.parent().map(|dir| dir.as_os_str()); match (existing_path, node_bin_dir) { @@ -606,15 +611,15 @@ impl NodeRuntimeTrait for ManagedNodeRuntime { #[derive(Debug, Clone)] pub struct SystemNodeRuntime { - node: PathBuf, - npm: PathBuf, - global_node_modules: PathBuf, - scratch_dir: PathBuf, + node: SanitizedPathBuf, + npm: SanitizedPathBuf, + global_node_modules: SanitizedPathBuf, + scratch_dir: SanitizedPathBuf, } impl SystemNodeRuntime { const MIN_VERSION: semver::Version = Version::new(20, 0, 0); - async fn new(node: PathBuf, npm: PathBuf) -> Result { + async fn new(node: SanitizedPathBuf, npm: SanitizedPathBuf) -> Result { let output = util::command::new_smol_command(&node) .arg("--version") .output() @@ -645,19 +650,19 @@ impl SystemNodeRuntime { let mut this = Self { node, npm, - global_node_modules: PathBuf::default(), + global_node_modules: SanitizedPathBuf::default(), scratch_dir, }; let output = this.run_npm_subcommand(None, None, "root", &["-g"]).await?; this.global_node_modules = - PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()); + PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()).into(); Ok(this) } async fn detect() -> std::result::Result { - let node = which::which("node").map_err(DetectError::NotInPath)?; - let npm = which::which("npm").map_err(DetectError::NotInPath)?; + let node = which::which("node").map_err(DetectError::NotInPath)?.into(); + let npm = which::which("npm").map_err(DetectError::NotInPath)?.into(); Self::new(node, npm).await.map_err(DetectError::Other) } } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index ede42af0272902892afd2e9dfdafb5c5eae2f8f5..9f0f6add324b9c842e36572fec84ebfb4c89648f 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; pub use util::paths::home_dir; +use util::paths::{SanitizedPath, SanitizedPathBuf}; /// A default editorconfig file name to use when resolving project settings. pub const EDITORCONFIG_NAME: &str = ".editorconfig"; @@ -12,30 +13,30 @@ pub const EDITORCONFIG_NAME: &str = ".editorconfig"; /// 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. -static CUSTOM_DATA_DIR: OnceLock = OnceLock::new(); +static CUSTOM_DATA_DIR: OnceLock = OnceLock::new(); /// The resolved data directory, combining custom override or platform defaults. /// This is set once and cached for subsequent calls. /// On macOS, this is `~/Library/Application Support/Zed`. /// On Linux/FreeBSD, this is `$XDG_DATA_HOME/zed`. /// On Windows, this is `%LOCALAPPDATA%\Zed`. -static CURRENT_DATA_DIR: OnceLock = OnceLock::new(); +static CURRENT_DATA_DIR: OnceLock = OnceLock::new(); /// The resolved config directory, combining custom override or platform defaults. /// This is set once and cached for subsequent calls. /// On macOS, this is `~/.config/zed`. /// On Linux/FreeBSD, this is `$XDG_CONFIG_HOME/zed`. /// On Windows, this is `%APPDATA%\Zed`. -static CONFIG_DIR: OnceLock = OnceLock::new(); +static CONFIG_DIR: OnceLock = OnceLock::new(); /// Returns the relative path to the zed_server directory on the ssh host. -pub fn remote_server_dir_relative() -> &'static Path { - Path::new(".zed_server") +pub fn remote_server_dir_relative() -> &'static SanitizedPath { + SanitizedPath::new(".zed_server") } /// Returns the relative path to the zed_wsl_server directory on the wsl host. -pub fn remote_wsl_server_dir_relative() -> &'static Path { - Path::new(".zed_wsl_server") +pub fn remote_wsl_server_dir_relative() -> &'static SanitizedPath { + SanitizedPath::new(".zed_wsl_server") } /// Sets a custom directory for all user data, overriding the default data directory. @@ -58,12 +59,12 @@ pub fn remote_wsl_server_dir_relative() -> &'static Path { /// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`) /// * The directory's path cannot be canonicalized to an absolute path /// * The directory cannot be created -pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { +pub fn set_custom_data_dir(dir: &str) -> &'static SanitizedPathBuf { if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() { panic!("set_custom_data_dir called after data_dir or config_dir was initialized"); } CUSTOM_DATA_DIR.get_or_init(|| { - let mut path = PathBuf::from(dir); + let mut path = SanitizedPathBuf::from(dir); if path.is_relative() { let abs_path = path .canonicalize() @@ -76,7 +77,7 @@ pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { } /// Returns the path to the configuration directory used by Zed. -pub fn config_dir() -> &'static PathBuf { +pub fn config_dir() -> &'static SanitizedPathBuf { CONFIG_DIR.get_or_init(|| { if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { custom_dir.join("config") @@ -84,6 +85,7 @@ pub fn config_dir() -> &'static PathBuf { dirs::config_dir() .expect("failed to determine RoamingAppData directory") .join("Zed") + .into() } 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() @@ -91,6 +93,7 @@ pub fn config_dir() -> &'static PathBuf { dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory") } .join("zed") + .into() } else { home_dir().join(".config").join("zed") } @@ -98,7 +101,7 @@ pub fn config_dir() -> &'static PathBuf { } /// Returns the path to the data directory used by Zed. -pub fn data_dir() -> &'static PathBuf { +pub fn data_dir() -> &'static SanitizedPathBuf { CURRENT_DATA_DIR.get_or_init(|| { if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { custom_dir.clone() @@ -111,10 +114,12 @@ pub fn data_dir() -> &'static PathBuf { dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory") } .join("zed") + .into() } else if cfg!(target_os = "windows") { dirs::data_local_dir() .expect("failed to determine LocalAppData directory") .join("Zed") + .into() } else { config_dir().clone() // Fallback } @@ -122,19 +127,21 @@ pub fn data_dir() -> &'static PathBuf { } /// Returns the path to the temp directory used by Zed. -pub fn temp_dir() -> &'static PathBuf { - static TEMP_DIR: OnceLock = OnceLock::new(); +pub fn temp_dir() -> &'static SanitizedPathBuf { + static TEMP_DIR: OnceLock = OnceLock::new(); TEMP_DIR.get_or_init(|| { if cfg!(target_os = "macos") { return dirs::cache_dir() .expect("failed to determine cachesDirectory directory") - .join("Zed"); + .join("Zed") + .into(); } if cfg!(target_os = "windows") { return dirs::cache_dir() .expect("failed to determine LocalAppData directory") - .join("Zed"); + .join("Zed") + .into(); } if cfg!(any(target_os = "linux", target_os = "freebsd")) { @@ -143,7 +150,8 @@ pub fn temp_dir() -> &'static PathBuf { } else { dirs::cache_dir().expect("failed to determine XDG_CACHE_HOME directory") } - .join("zed"); + .join("zed") + .into(); } home_dir().join(".cache").join("zed") @@ -151,8 +159,8 @@ pub fn temp_dir() -> &'static PathBuf { } /// Returns the path to the logs directory. -pub fn logs_dir() -> &'static PathBuf { - static LOGS_DIR: OnceLock = OnceLock::new(); +pub fn logs_dir() -> &'static SanitizedPathBuf { + static LOGS_DIR: OnceLock = OnceLock::new(); LOGS_DIR.get_or_init(|| { if cfg!(target_os = "macos") { home_dir().join("Library/Logs/Zed") @@ -163,128 +171,128 @@ pub fn logs_dir() -> &'static PathBuf { } /// Returns the path to the Zed server directory on this SSH host. -pub fn remote_server_state_dir() -> &'static PathBuf { - static REMOTE_SERVER_STATE: OnceLock = OnceLock::new(); +pub fn remote_server_state_dir() -> &'static SanitizedPathBuf { + static REMOTE_SERVER_STATE: OnceLock = OnceLock::new(); REMOTE_SERVER_STATE.get_or_init(|| data_dir().join("server_state")) } /// Returns the path to the `Zed.log` file. -pub fn log_file() -> &'static PathBuf { - static LOG_FILE: OnceLock = OnceLock::new(); +pub fn log_file() -> &'static SanitizedPathBuf { + static LOG_FILE: OnceLock = OnceLock::new(); LOG_FILE.get_or_init(|| logs_dir().join("Zed.log")) } /// Returns the path to the `Zed.log.old` file. -pub fn old_log_file() -> &'static PathBuf { - static OLD_LOG_FILE: OnceLock = OnceLock::new(); +pub fn old_log_file() -> &'static SanitizedPathBuf { + static OLD_LOG_FILE: OnceLock = OnceLock::new(); OLD_LOG_FILE.get_or_init(|| logs_dir().join("Zed.log.old")) } /// Returns the path to the database directory. -pub fn database_dir() -> &'static PathBuf { - static DATABASE_DIR: OnceLock = OnceLock::new(); +pub fn database_dir() -> &'static SanitizedPathBuf { + static DATABASE_DIR: OnceLock = OnceLock::new(); DATABASE_DIR.get_or_init(|| data_dir().join("db")) } /// Returns the path to the crashes directory, if it exists for the current platform. -pub fn crashes_dir() -> &'static Option { - static CRASHES_DIR: OnceLock> = OnceLock::new(); +pub fn crashes_dir() -> &'static Option { + static CRASHES_DIR: OnceLock> = OnceLock::new(); CRASHES_DIR.get_or_init(|| { cfg!(target_os = "macos").then_some(home_dir().join("Library/Logs/DiagnosticReports")) }) } /// Returns the path to the retired crashes directory, if it exists for the current platform. -pub fn crashes_retired_dir() -> &'static Option { - static CRASHES_RETIRED_DIR: OnceLock> = OnceLock::new(); +pub fn crashes_retired_dir() -> &'static Option { + static CRASHES_RETIRED_DIR: OnceLock> = OnceLock::new(); CRASHES_RETIRED_DIR.get_or_init(|| crashes_dir().as_ref().map(|dir| dir.join("Retired"))) } /// Returns the path to the `settings.json` file. -pub fn settings_file() -> &'static PathBuf { - static SETTINGS_FILE: OnceLock = OnceLock::new(); +pub fn settings_file() -> &'static SanitizedPathBuf { + static SETTINGS_FILE: OnceLock = OnceLock::new(); SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json")) } /// Returns the path to the global settings file. -pub fn global_settings_file() -> &'static PathBuf { - static GLOBAL_SETTINGS_FILE: OnceLock = OnceLock::new(); +pub fn global_settings_file() -> &'static SanitizedPathBuf { + static GLOBAL_SETTINGS_FILE: OnceLock = OnceLock::new(); GLOBAL_SETTINGS_FILE.get_or_init(|| config_dir().join("global_settings.json")) } /// Returns the path to the `settings_backup.json` file. -pub fn settings_backup_file() -> &'static PathBuf { - static SETTINGS_FILE: OnceLock = OnceLock::new(); +pub fn settings_backup_file() -> &'static SanitizedPathBuf { + static SETTINGS_FILE: OnceLock = OnceLock::new(); SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json")) } /// Returns the path to the `keymap.json` file. -pub fn keymap_file() -> &'static PathBuf { - static KEYMAP_FILE: OnceLock = OnceLock::new(); +pub fn keymap_file() -> &'static SanitizedPathBuf { + static KEYMAP_FILE: OnceLock = OnceLock::new(); KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json")) } /// Returns the path to the `keymap_backup.json` file. -pub fn keymap_backup_file() -> &'static PathBuf { - static KEYMAP_FILE: OnceLock = OnceLock::new(); +pub fn keymap_backup_file() -> &'static SanitizedPathBuf { + static KEYMAP_FILE: OnceLock = OnceLock::new(); KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json")) } /// Returns the path to the `tasks.json` file. -pub fn tasks_file() -> &'static PathBuf { - static TASKS_FILE: OnceLock = OnceLock::new(); +pub fn tasks_file() -> &'static SanitizedPathBuf { + static TASKS_FILE: OnceLock = OnceLock::new(); TASKS_FILE.get_or_init(|| config_dir().join("tasks.json")) } /// Returns the path to the `debug.json` file. -pub fn debug_scenarios_file() -> &'static PathBuf { - static DEBUG_SCENARIOS_FILE: OnceLock = OnceLock::new(); +pub fn debug_scenarios_file() -> &'static SanitizedPathBuf { + static DEBUG_SCENARIOS_FILE: OnceLock = OnceLock::new(); DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json")) } /// Returns the path to the extensions directory. /// /// This is where installed extensions are stored. -pub fn extensions_dir() -> &'static PathBuf { - static EXTENSIONS_DIR: OnceLock = OnceLock::new(); +pub fn extensions_dir() -> &'static SanitizedPathBuf { + static EXTENSIONS_DIR: OnceLock = OnceLock::new(); EXTENSIONS_DIR.get_or_init(|| data_dir().join("extensions")) } /// Returns the path to the extensions directory. /// /// This is where installed extensions are stored on a remote. -pub fn remote_extensions_dir() -> &'static PathBuf { - static EXTENSIONS_DIR: OnceLock = OnceLock::new(); +pub fn remote_extensions_dir() -> &'static SanitizedPathBuf { + static EXTENSIONS_DIR: OnceLock = OnceLock::new(); EXTENSIONS_DIR.get_or_init(|| data_dir().join("remote_extensions")) } /// Returns the path to the extensions directory. /// /// This is where installed extensions are stored on a remote. -pub fn remote_extensions_uploads_dir() -> &'static PathBuf { - static UPLOAD_DIR: OnceLock = OnceLock::new(); +pub fn remote_extensions_uploads_dir() -> &'static SanitizedPathBuf { + static UPLOAD_DIR: OnceLock = OnceLock::new(); UPLOAD_DIR.get_or_init(|| remote_extensions_dir().join("uploads")) } /// Returns the path to the themes directory. /// /// This is where themes that are not provided by extensions are stored. -pub fn themes_dir() -> &'static PathBuf { - static THEMES_DIR: OnceLock = OnceLock::new(); +pub fn themes_dir() -> &'static SanitizedPathBuf { + static THEMES_DIR: OnceLock = OnceLock::new(); THEMES_DIR.get_or_init(|| config_dir().join("themes")) } /// Returns the path to the snippets directory. -pub fn snippets_dir() -> &'static PathBuf { - static SNIPPETS_DIR: OnceLock = OnceLock::new(); +pub fn snippets_dir() -> &'static SanitizedPathBuf { + static SNIPPETS_DIR: OnceLock = OnceLock::new(); SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets")) } /// Returns the path to the contexts directory. /// /// This is where the saved contexts from the Assistant are stored. -pub fn contexts_dir() -> &'static PathBuf { - static CONTEXTS_DIR: OnceLock = OnceLock::new(); +pub fn contexts_dir() -> &'static SanitizedPathBuf { + static CONTEXTS_DIR: OnceLock = OnceLock::new(); CONTEXTS_DIR.get_or_init(|| { if cfg!(target_os = "macos") { config_dir().join("conversations") @@ -297,8 +305,8 @@ pub fn contexts_dir() -> &'static PathBuf { /// Returns the path to the contexts directory. /// /// This is where the prompts for use with the Assistant are stored. -pub fn prompts_dir() -> &'static PathBuf { - static PROMPTS_DIR: OnceLock = OnceLock::new(); +pub fn prompts_dir() -> &'static SanitizedPathBuf { + static PROMPTS_DIR: OnceLock = OnceLock::new(); PROMPTS_DIR.get_or_init(|| { if cfg!(target_os = "macos") { config_dir().join("prompts") @@ -315,15 +323,15 @@ pub fn prompts_dir() -> &'static PathBuf { /// # Arguments /// /// * `dev_mode` - If true, assumes the current working directory is the Zed repository. -pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf { +pub fn prompt_overrides_dir(repo_path: Option<&SanitizedPath>) -> SanitizedPathBuf { if let Some(path) = repo_path { let dev_path = path.join("assets").join("prompts"); if dev_path.exists() { - return dev_path; + return dev_path.into(); } } - static PROMPT_TEMPLATES_DIR: OnceLock = OnceLock::new(); + static PROMPT_TEMPLATES_DIR: OnceLock = OnceLock::new(); PROMPT_TEMPLATES_DIR .get_or_init(|| { if cfg!(target_os = "macos") { @@ -338,8 +346,8 @@ pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf { /// Returns the path to the semantic search's embeddings directory. /// /// This is where the embeddings used to power semantic search are stored. -pub fn embeddings_dir() -> &'static PathBuf { - static EMBEDDINGS_DIR: OnceLock = OnceLock::new(); +pub fn embeddings_dir() -> &'static SanitizedPathBuf { + static EMBEDDINGS_DIR: OnceLock = OnceLock::new(); EMBEDDINGS_DIR.get_or_init(|| { if cfg!(target_os = "macos") { config_dir().join("embeddings") @@ -352,74 +360,74 @@ pub fn embeddings_dir() -> &'static PathBuf { /// Returns the path to the languages directory. /// /// This is where language servers are downloaded to for languages built-in to Zed. -pub fn languages_dir() -> &'static PathBuf { - static LANGUAGES_DIR: OnceLock = OnceLock::new(); +pub fn languages_dir() -> &'static SanitizedPathBuf { + static LANGUAGES_DIR: OnceLock = OnceLock::new(); LANGUAGES_DIR.get_or_init(|| data_dir().join("languages")) } /// Returns the path to the debug adapters directory /// /// This is where debug adapters are downloaded to for DAPs that are built-in to Zed. -pub fn debug_adapters_dir() -> &'static PathBuf { - static DEBUG_ADAPTERS_DIR: OnceLock = OnceLock::new(); +pub fn debug_adapters_dir() -> &'static SanitizedPathBuf { + static DEBUG_ADAPTERS_DIR: OnceLock = OnceLock::new(); DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters")) } /// Returns the path to the agent servers directory /// /// This is where agent servers are downloaded to -pub fn agent_servers_dir() -> &'static PathBuf { - static AGENT_SERVERS_DIR: OnceLock = OnceLock::new(); +pub fn agent_servers_dir() -> &'static SanitizedPathBuf { + static AGENT_SERVERS_DIR: OnceLock = OnceLock::new(); AGENT_SERVERS_DIR.get_or_init(|| data_dir().join("agent_servers")) } /// Returns the path to the Copilot directory. -pub fn copilot_dir() -> &'static PathBuf { - static COPILOT_DIR: OnceLock = OnceLock::new(); +pub fn copilot_dir() -> &'static SanitizedPathBuf { + static COPILOT_DIR: OnceLock = OnceLock::new(); COPILOT_DIR.get_or_init(|| data_dir().join("copilot")) } /// Returns the path to the Supermaven directory. -pub fn supermaven_dir() -> &'static PathBuf { - static SUPERMAVEN_DIR: OnceLock = OnceLock::new(); +pub fn supermaven_dir() -> &'static SanitizedPathBuf { + static SUPERMAVEN_DIR: OnceLock = OnceLock::new(); SUPERMAVEN_DIR.get_or_init(|| data_dir().join("supermaven")) } /// Returns the path to the default Prettier directory. -pub fn default_prettier_dir() -> &'static PathBuf { - static DEFAULT_PRETTIER_DIR: OnceLock = OnceLock::new(); +pub fn default_prettier_dir() -> &'static SanitizedPathBuf { + static DEFAULT_PRETTIER_DIR: OnceLock = OnceLock::new(); DEFAULT_PRETTIER_DIR.get_or_init(|| data_dir().join("prettier")) } /// Returns the path to the remote server binaries directory. -pub fn remote_servers_dir() -> &'static PathBuf { - static REMOTE_SERVERS_DIR: OnceLock = OnceLock::new(); +pub fn remote_servers_dir() -> &'static SanitizedPathBuf { + static REMOTE_SERVERS_DIR: OnceLock = OnceLock::new(); REMOTE_SERVERS_DIR.get_or_init(|| data_dir().join("remote_servers")) } /// Returns the relative path to a `.zed` folder within a project. -pub fn local_settings_folder_relative_path() -> &'static Path { - Path::new(".zed") +pub fn local_settings_folder_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".zed") } /// Returns the relative path to a `.vscode` folder within a project. -pub fn local_vscode_folder_relative_path() -> &'static Path { - Path::new(".vscode") +pub fn local_vscode_folder_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".vscode") } /// Returns the relative path to a `settings.json` file within a project. -pub fn local_settings_file_relative_path() -> &'static Path { - Path::new(".zed/settings.json") +pub fn local_settings_file_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".zed/settings.json") } /// Returns the relative path to a `tasks.json` file within a project. -pub fn local_tasks_file_relative_path() -> &'static Path { - Path::new(".zed/tasks.json") +pub fn local_tasks_file_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".zed/tasks.json") } /// Returns the relative path to a `.vscode/tasks.json` file within a project. -pub fn local_vscode_tasks_file_relative_path() -> &'static Path { - Path::new(".vscode/tasks.json") +pub fn local_vscode_tasks_file_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".vscode/tasks.json") } pub fn debug_task_file_name() -> &'static str { @@ -432,25 +440,25 @@ pub fn task_file_name() -> &'static str { /// Returns the relative path to a `debug.json` file within a project. /// .zed/debug.json -pub fn local_debug_file_relative_path() -> &'static Path { - Path::new(".zed/debug.json") +pub fn local_debug_file_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".zed/debug.json") } /// Returns the relative path to a `.vscode/launch.json` file within a project. -pub fn local_vscode_launch_file_relative_path() -> &'static Path { - Path::new(".vscode/launch.json") +pub fn local_vscode_launch_file_relative_path() -> &'static SanitizedPath { + SanitizedPath::new(".vscode/launch.json") } -pub fn user_ssh_config_file() -> PathBuf { +pub fn user_ssh_config_file() -> SanitizedPathBuf { home_dir().join(".ssh/config") } -pub fn global_ssh_config_file() -> &'static Path { - Path::new("/etc/ssh/ssh_config") +pub fn global_ssh_config_file() -> &'static SanitizedPath { + SanitizedPath::new("/etc/ssh/ssh_config") } /// Returns candidate paths for the vscode user settings file -pub fn vscode_settings_file_paths() -> Vec { +pub fn vscode_settings_file_paths() -> Vec { let mut paths = vscode_user_data_paths(); for path in paths.iter_mut() { path.push("User/settings.json"); @@ -459,7 +467,7 @@ pub fn vscode_settings_file_paths() -> Vec { } /// Returns candidate paths for the cursor user settings file -pub fn cursor_settings_file_paths() -> Vec { +pub fn cursor_settings_file_paths() -> Vec { let mut paths = cursor_user_data_paths(); for path in paths.iter_mut() { path.push("User/settings.json"); @@ -467,7 +475,7 @@ pub fn cursor_settings_file_paths() -> Vec { paths } -fn vscode_user_data_paths() -> Vec { +fn vscode_user_data_paths() -> Vec { // https://github.com/microsoft/vscode/blob/23e7148cdb6d8a27f0109ff77e5b1e019f8da051/src/vs/platform/environment/node/userDataPath.ts#L45 const VSCODE_PRODUCT_NAMES: &[&str] = &[ "Code", @@ -479,11 +487,11 @@ fn vscode_user_data_paths() -> Vec { ]; let mut paths = Vec::new(); if let Ok(portable_path) = env::var("VSCODE_PORTABLE") { - paths.push(Path::new(&portable_path).join("user-data")); + paths.push(SanitizedPath::new(&portable_path).join("user-data")); } if let Ok(vscode_appdata) = env::var("VSCODE_APPDATA") { for product_name in VSCODE_PRODUCT_NAMES { - paths.push(Path::new(&vscode_appdata).join(product_name)); + paths.push(SanitizedPath::new(&vscode_appdata).join(product_name)); } } for product_name in VSCODE_PRODUCT_NAMES { @@ -492,13 +500,13 @@ fn vscode_user_data_paths() -> Vec { paths } -fn cursor_user_data_paths() -> Vec { +fn cursor_user_data_paths() -> Vec { let mut paths = Vec::new(); add_vscode_user_data_paths(&mut paths, "Cursor"); paths } -fn add_vscode_user_data_paths(paths: &mut Vec, product_name: &str) { +fn add_vscode_user_data_paths(paths: &mut Vec, product_name: &str) { if cfg!(target_os = "macos") { paths.push( home_dir() @@ -507,14 +515,15 @@ fn add_vscode_user_data_paths(paths: &mut Vec, product_name: &str) { ); } else if cfg!(target_os = "windows") { if let Some(data_local_dir) = dirs::data_local_dir() { - paths.push(data_local_dir.join(product_name)); + paths.push(data_local_dir.join(product_name).into()); } if let Some(data_dir) = dirs::data_dir() { - paths.push(data_dir.join(product_name)); + paths.push(data_dir.join(product_name).into()); } } else { paths.push( dirs::config_dir() + .map(|e| e.into()) .unwrap_or(home_dir().join(".config")) .join(product_name), ); diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs index f677a3b96728a574416cbfc1ec97799ac19184fa..85f5f2655a761b0a7a7fa6837d3a1e5c1a30d12d 100644 --- a/crates/proto/src/typed_envelope.rs +++ b/crates/proto/src/typed_envelope.rs @@ -210,15 +210,12 @@ impl FromProto for Arc { } } -impl ToProto for PathBuf { +impl ToProto for T +where + T: AsRef, +{ fn to_proto(self) -> String { - to_proto_path(&self) - } -} - -impl ToProto for &Path { - fn to_proto(self) -> String { - to_proto_path(self) + to_proto_path(self.as_ref()) } } diff --git a/crates/supermaven_api/src/supermaven_api.rs b/crates/supermaven_api/src/supermaven_api.rs index c4b1409d646b0d402684d3c883a0ea633d12bbb5..08fdfd1a0926205349c7447e2949816c710f2f9a 100644 --- a/crates/supermaven_api/src/supermaven_api.rs +++ b/crates/supermaven_api/src/supermaven_api.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use smol::fs::{self, File}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use util::paths::{SanitizedPath, SanitizedPathBuf}; use util::fs::{make_file_executable, remove_matching}; @@ -212,7 +213,7 @@ pub async fn latest_release( .with_context(|| "Unable to parse Supermaven Agent response".to_string()) } -pub fn version_path(version: u64) -> PathBuf { +pub fn version_path(version: u64) -> SanitizedPathBuf { supermaven_dir().join(format!( "sm-agent-{}{}", version, @@ -220,11 +221,11 @@ pub fn version_path(version: u64) -> PathBuf { )) } -pub async fn has_version(version_path: &Path) -> bool { +pub async fn has_version(version_path: &SanitizedPath) -> bool { fs::metadata(version_path).await.is_ok_and(|m| m.is_file()) } -pub async fn get_supermaven_agent_path(client: Arc) -> Result { +pub async fn get_supermaven_agent_path(client: Arc) -> Result { fs::create_dir_all(supermaven_dir()) .await .with_context(|| { @@ -277,7 +278,7 @@ pub async fn get_supermaven_agent_path(client: Arc) -> Result(dir: &Path, predicate: F) +pub async fn remove_matching(dir: &SanitizedPath, predicate: F) where - F: Fn(&Path) -> bool, + F: Fn(&SanitizedPath) -> bool, { if let Some(mut entries) = fs::read_dir(dir).await.log_err() { while let Some(entry) = entries.next().await { if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); + let entry_path: SanitizedPathBuf = entry.path().into(); if predicate(entry_path.as_path()) && let Ok(metadata) = fs::metadata(&entry_path).await { @@ -27,18 +30,18 @@ where } } -pub async fn collect_matching(dir: &Path, predicate: F) -> Vec +pub async fn collect_matching(dir: &SanitizedPath, predicate: F) -> Vec where - F: Fn(&Path) -> bool, + F: Fn(&SanitizedPath) -> bool, { let mut matching = vec![]; if let Some(mut entries) = fs::read_dir(dir).await.log_err() { while let Some(entry) = entries.next().await { if let Some(entry) = entry.log_err() - && predicate(entry.path().as_path()) + && predicate(SanitizedPath::new(entry.path().as_path())) { - matching.push(entry.path()); + matching.push(entry.path().into()); } } } @@ -46,7 +49,7 @@ where matching } -pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option +pub async fn find_file_name_in_dir(dir: &SanitizedPath, predicate: F) -> Option where F: Fn(&str) -> bool, { @@ -60,7 +63,7 @@ where .map(|file_name| file_name.to_string_lossy()) && predicate(&file_name) { - return Some(entry_path); + return Some(entry_path.into()); } } } @@ -94,9 +97,9 @@ pub async fn move_folder_files_to_folder>( #[cfg(unix)] /// Set the permissions for the given path so that the file becomes executable. /// This is a noop for non-unix platforms. -pub async fn make_file_executable(path: &Path) -> std::io::Result<()> { +pub async fn make_file_executable(path: &SanitizedPath) -> std::io::Result<()> { fs::set_permissions( - path, + path.as_path(), ::from_mode(0o755), ) .await @@ -106,6 +109,6 @@ pub async fn make_file_executable(path: &Path) -> std::io::Result<()> { #[allow(clippy::unused_async)] /// Set the permissions for the given path so that the file becomes executable. /// This is a noop for non-unix platforms. -pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> { +pub async fn make_file_executable(_path: &SanitizedPath) -> std::io::Result<()> { Ok(()) } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 318900d540172035b29ae25ad5f42dbbac87bf60..9fb57204b67414ba8b3111c478756b25c3405161 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,9 +1,11 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; use regex::Regex; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::cmp::Ordering; use std::fmt::{Display, Formatter}; use std::mem; +use std::ops::Deref; use std::path::StripPrefixError; use std::sync::{Arc, OnceLock}; use std::{ @@ -13,9 +15,13 @@ use std::{ }; /// Returns the path to the user's home directory. -pub fn home_dir() -> &'static PathBuf { - static HOME_DIR: OnceLock = OnceLock::new(); - HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory")) +pub fn home_dir() -> &'static SanitizedPathBuf { + static HOME_DIR: OnceLock = OnceLock::new(); + HOME_DIR.get_or_init(|| { + dirs::home_dir() + .expect("failed to determine home directory") + .into() + }) } pub trait PathExt { @@ -160,8 +166,8 @@ impl SanitizedPath { self.0.extension() } - pub fn join>(&self, path: P) -> PathBuf { - self.0.join(path) + pub fn join>(&self, path: P) -> SanitizedPathBuf { + self.0.join(path).into() } pub fn parent(&self) -> Option<&Self> { @@ -224,6 +230,116 @@ impl AsRef for SanitizedPath { } } +/// In memory, this is identical to `PathBuf`. On non-Windows conversions to this type are no-ops. On +/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix. +#[derive(Default, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +#[repr(transparent)] +pub struct SanitizedPathBuf(PathBuf); + +impl SanitizedPathBuf { + pub fn new() -> Self { + PathBuf::new().into() + } + + pub fn exists(&self) -> bool { + self.0.exists() + } + + pub fn push>(&mut self, path: P) { + if path.as_ref().is_absolute() { + self.0.push(SanitizedPath::new(path.as_ref()).as_path()); + } else { + self.0.push(path); + } + } + + pub fn is_relative(&self) -> bool { + self.0.is_relative() + } + + pub fn canonicalize(&self) -> std::io::Result { + Ok(self.0.canonicalize()?.into()) + } + + pub fn join>(&self, path: P) -> SanitizedPathBuf { + self.0.join(SanitizedPath::new(path.as_ref())).into() + } + + pub fn display(&self) -> std::path::Display<'_> { + self.0.display() + } + + pub fn to_string_lossy(&self) -> Cow<'_, str> { + self.0.to_string_lossy() + } + + pub fn as_path(&self) -> &SanitizedPath { + SanitizedPath::unchecked_new(self) + } +} + +impl std::fmt::Debug for SanitizedPathBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl AsRef for SanitizedPathBuf { + fn as_ref(&self) -> &PathBuf { + &self.0 + } +} + +impl Deref for SanitizedPathBuf { + type Target = SanitizedPath; + + fn deref(&self) -> &Self::Target { + self.as_path() + } +} + +impl AsRef for SanitizedPathBuf { + fn as_ref(&self) -> &SanitizedPath { + self.as_path() + } +} + +impl AsRef for SanitizedPathBuf { + fn as_ref(&self) -> &OsStr { + self.0.as_os_str() + } +} + +impl AsRef for SanitizedPathBuf { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +impl From for SanitizedPathBuf { + fn from(path: PathBuf) -> Self { + Self::from(path.as_path()) + } +} + +impl From<&SanitizedPath> for SanitizedPathBuf { + fn from(value: &SanitizedPath) -> Self { + Self(PathBuf::from(value.as_path())) + } +} + +impl From<&Path> for SanitizedPathBuf { + fn from(path: &Path) -> Self { + Self(PathBuf::from(dunce::simplified(path))) + } +} + +impl From<&str> for SanitizedPathBuf { + fn from(path: &str) -> Self { + PathBuf::from(path).into() + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PathStyle { Posix, @@ -759,11 +875,11 @@ fn natural_sort(a: &str, b: &str) -> Ordering { } pub fn compare_paths( - (path_a, a_is_file): (&Path, bool), - (path_b, b_is_file): (&Path, bool), + (path_a, a_is_file): (&SanitizedPath, bool), + (path_b, b_is_file): (&SanitizedPath, bool), ) -> Ordering { - let mut components_a = path_a.components().peekable(); - let mut components_b = path_b.components().peekable(); + let mut components_a = path_a.as_path().components().peekable(); + let mut components_b = path_b.as_path().components().peekable(); loop { match (components_a.next(), components_b.next()) { @@ -824,37 +940,37 @@ mod tests { #[test] fn compare_paths_with_dots() { let mut paths = vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/1.46"), false), - (Path::new("test_dirs/1.46/bar_1"), true), - (Path::new("test_dirs/1.46/bar_2"), true), - (Path::new("test_dirs/1.45"), false), - (Path::new("test_dirs/1.45/foo_2"), true), - (Path::new("test_dirs/1.45/foo_1"), true), + (SanitizedPath::new("test_dirs"), false), + (SanitizedPath::new("test_dirs/1.46"), false), + (SanitizedPath::new("test_dirs/1.46/bar_1"), true), + (SanitizedPath::new("test_dirs/1.46/bar_2"), true), + (SanitizedPath::new("test_dirs/1.45"), false), + (SanitizedPath::new("test_dirs/1.45/foo_2"), true), + (SanitizedPath::new("test_dirs/1.45/foo_1"), true), ]; paths.sort_by(|&a, &b| compare_paths(a, b)); assert_eq!( paths, vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/1.45"), false), - (Path::new("test_dirs/1.45/foo_1"), true), - (Path::new("test_dirs/1.45/foo_2"), true), - (Path::new("test_dirs/1.46"), false), - (Path::new("test_dirs/1.46/bar_1"), true), - (Path::new("test_dirs/1.46/bar_2"), true), + (SanitizedPath::new("test_dirs"), false), + (SanitizedPath::new("test_dirs/1.45"), false), + (SanitizedPath::new("test_dirs/1.45/foo_1"), true), + (SanitizedPath::new("test_dirs/1.45/foo_2"), true), + (SanitizedPath::new("test_dirs/1.46"), false), + (SanitizedPath::new("test_dirs/1.46/bar_1"), true), + (SanitizedPath::new("test_dirs/1.46/bar_2"), true), ] ); let mut paths = vec![ - (Path::new("root1/one.txt"), true), - (Path::new("root1/one.two.txt"), true), + (SanitizedPath::new("root1/one.txt"), true), + (SanitizedPath::new("root1/one.two.txt"), true), ]; paths.sort_by(|&a, &b| compare_paths(a, b)); assert_eq!( paths, vec![ - (Path::new("root1/one.txt"), true), - (Path::new("root1/one.two.txt"), true), + (SanitizedPath::new("root1/one.txt"), true), + (SanitizedPath::new("root1/one.two.txt"), true), ] ); } @@ -862,21 +978,21 @@ mod tests { #[test] fn compare_paths_with_same_name_different_extensions() { let mut paths = vec![ - (Path::new("test_dirs/file.rs"), true), - (Path::new("test_dirs/file.txt"), true), - (Path::new("test_dirs/file.md"), true), - (Path::new("test_dirs/file"), true), - (Path::new("test_dirs/file.a"), true), + (SanitizedPath::new("test_dirs/file.rs"), true), + (SanitizedPath::new("test_dirs/file.txt"), true), + (SanitizedPath::new("test_dirs/file.md"), true), + (SanitizedPath::new("test_dirs/file"), true), + (SanitizedPath::new("test_dirs/file.a"), true), ]; paths.sort_by(|&a, &b| compare_paths(a, b)); assert_eq!( paths, vec![ - (Path::new("test_dirs/file"), true), - (Path::new("test_dirs/file.a"), true), - (Path::new("test_dirs/file.md"), true), - (Path::new("test_dirs/file.rs"), true), - (Path::new("test_dirs/file.txt"), true), + (SanitizedPath::new("test_dirs/file"), true), + (SanitizedPath::new("test_dirs/file.a"), true), + (SanitizedPath::new("test_dirs/file.md"), true), + (SanitizedPath::new("test_dirs/file.rs"), true), + (SanitizedPath::new("test_dirs/file.txt"), true), ] ); } @@ -884,31 +1000,31 @@ mod tests { #[test] fn compare_paths_case_semi_sensitive() { let mut paths = vec![ - (Path::new("test_DIRS"), false), - (Path::new("test_DIRS/foo_1"), true), - (Path::new("test_DIRS/foo_2"), true), - (Path::new("test_DIRS/bar"), true), - (Path::new("test_DIRS/BAR"), true), - (Path::new("test_dirs"), false), - (Path::new("test_dirs/foo_1"), true), - (Path::new("test_dirs/foo_2"), true), - (Path::new("test_dirs/bar"), true), - (Path::new("test_dirs/BAR"), true), + (SanitizedPath::new("test_DIRS"), false), + (SanitizedPath::new("test_DIRS/foo_1"), true), + (SanitizedPath::new("test_DIRS/foo_2"), true), + (SanitizedPath::new("test_DIRS/bar"), true), + (SanitizedPath::new("test_DIRS/BAR"), true), + (SanitizedPath::new("test_dirs"), false), + (SanitizedPath::new("test_dirs/foo_1"), true), + (SanitizedPath::new("test_dirs/foo_2"), true), + (SanitizedPath::new("test_dirs/bar"), true), + (SanitizedPath::new("test_dirs/BAR"), true), ]; paths.sort_by(|&a, &b| compare_paths(a, b)); assert_eq!( paths, vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/bar"), true), - (Path::new("test_dirs/BAR"), true), - (Path::new("test_dirs/foo_1"), true), - (Path::new("test_dirs/foo_2"), true), - (Path::new("test_DIRS"), false), - (Path::new("test_DIRS/bar"), true), - (Path::new("test_DIRS/BAR"), true), - (Path::new("test_DIRS/foo_1"), true), - (Path::new("test_DIRS/foo_2"), true), + (SanitizedPath::new("test_dirs"), false), + (SanitizedPath::new("test_dirs/bar"), true), + (SanitizedPath::new("test_dirs/BAR"), true), + (SanitizedPath::new("test_dirs/foo_1"), true), + (SanitizedPath::new("test_dirs/foo_2"), true), + (SanitizedPath::new("test_DIRS"), false), + (SanitizedPath::new("test_DIRS/bar"), true), + (SanitizedPath::new("test_DIRS/BAR"), true), + (SanitizedPath::new("test_DIRS/foo_1"), true), + (SanitizedPath::new("test_DIRS/foo_2"), true), ] ); } @@ -1393,7 +1509,10 @@ mod tests { fn test_compare_paths() { // Helper function for cleaner tests fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering { - compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file)) + compare_paths( + (SanitizedPath::new(a), is_a_file), + (SanitizedPath::new(b), is_b_file), + ) } // Basic path comparison diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 7af86d3364f6e07116ae701da83b897869bb905e..1ad6c9600d6caac0c9f3ac48deba7ad5bbd12040 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -736,7 +736,7 @@ impl Worktree { id: self.id().to_proto(), root_name: self.root_name().to_string(), visible: self.is_visible(), - abs_path: self.abs_path().to_proto(), + abs_path: self.abs_path().as_path().to_proto(), } } @@ -761,10 +761,10 @@ impl Worktree { } } - pub fn abs_path(&self) -> Arc { + pub fn abs_path(&self) -> Arc { match self { - Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), - Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), + Worktree::Local(worktree) => worktree.abs_path.clone(), + Worktree::Remote(worktree) => worktree.abs_path.clone(), } } @@ -1320,6 +1320,7 @@ impl LocalWorktree { cx: &mut Context, ) { let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot); + dbg!(&self.snapshot, &new_snapshot, &repo_changes, &entry_changes); self.snapshot = new_snapshot; if let Some(share) = self.update_observer.as_mut() { @@ -4245,6 +4246,7 @@ impl BackgroundScanner { fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool { let mut state = self.state.lock(); + dbg!("status_update", &state.snapshot, &scanning); if state.changed_paths.is_empty() && scanning { return true; } @@ -4767,10 +4769,12 @@ impl BackgroundScanner { } fn update_git_repositories(&self, dot_git_paths: Vec) { + dbg!("Updating git repositories", &dot_git_paths); log::trace!("reloading repositories: {dot_git_paths:?}"); let mut state = self.state.lock(); let scan_id = state.snapshot.scan_id; for dot_git_dir in dot_git_paths { + dbg!("available repos", &state.snapshot.git_repositories); let existing_repository_entry = state .snapshot @@ -4798,6 +4802,7 @@ impl BackgroundScanner { ); } Some(local_repository) => { + dbg!("Updating repo", &local_repository, scan_id); state.snapshot.git_repositories.update( &local_repository.work_directory_id, |entry| {