From a50f003c641ca5003008a3999ad6ed75e9eccf5c Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 5 Sep 2026 20:46:31 +0200 Subject: [PATCH 1/2] fix(gemset): stop calling std::env::split_paths on wasm std::env::split_paths is unimplemented for wasm32-wasip2 and panics with "unsupported". Gemset::env reached it whenever GEM_PATH was set in the worktree shell environment, so the extension aborted inside language_server_command. wasmtime then rejects every later call into the extension with "cannot enter component instance" until Zed restarts. Split GEM_PATH on ':' instead, matching how the value is joined when the gem home is prepended. --- src/gemset.rs | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/gemset.rs b/src/gemset.rs index 7c39ffe..245915d 100644 --- a/src/gemset.rs +++ b/src/gemset.rs @@ -82,10 +82,12 @@ impl Gemset { env_map .entry("GEM_PATH".to_string()) .and_modify(|existing_gem_path| { - let paths: Vec<_> = std::env::split_paths(existing_gem_path).collect(); - let gem_home_path = std::path::Path::new(&gem_path); + let gem_home_path = Path::new(&gem_path); + let already_listed = existing_gem_path + .split(':') + .any(|entry| Path::new(entry) == gem_home_path); - if !paths.iter().any(|p| p == gem_home_path) { + if !already_listed { *existing_gem_path = format!("{gem_path}:{existing_gem_path}"); } }) @@ -381,6 +383,31 @@ mod tests { assert_eq!(env.get("PATH").unwrap(), &format!("/usr/bin:{gem_bin}")); } + #[test] + fn test_gem_env_does_not_duplicate_gem_home_in_gem_path() { + let existing = format!("{TEST_GEM_PATH}:{TEST_GEM_HOME}"); + let gemset = Gemset::new( + TEST_GEM_HOME.into(), + Some(&[("GEM_PATH", &existing)]), + Box::new(MockCommandExecutor::new()), + ); + let env: std::collections::HashMap = gemset.env().iter().cloned().collect(); + + assert_eq!(env.get("GEM_PATH").unwrap(), &existing); + } + + #[test] + fn test_gem_env_without_gem_path() { + let gemset = Gemset::new( + TEST_GEM_HOME.into(), + Some(&[("PATH", "/usr/bin")]), + Box::new(MockCommandExecutor::new()), + ); + let env: std::collections::HashMap = gemset.env().iter().cloned().collect(); + + assert_eq!(env.get("GEM_PATH").unwrap(), TEST_GEM_HOME); + } + #[test] fn test_install_gem_success() { let mock_executor = MockCommandExecutor::new(); From 580534ba6330f3a1f9545fbdec260bbe0e0934ea Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 5 Sep 2026 20:51:52 +0200 Subject: [PATCH 2/2] fix(gemset): join and split GEM_PATH with the host path separator Gemset::env joined GEM_PATH and PATH with ':' on every host, which produces values Ruby cannot read on Windows. Take the host OS from zed::current_platform() and use ';' there. --- src/gemset.rs | 73 +++++++++++++++++++++++-- src/language_servers/language_server.rs | 1 + src/ruby.rs | 7 ++- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/gemset.rs b/src/gemset.rs index 245915d..1959946 100644 --- a/src/gemset.rs +++ b/src/gemset.rs @@ -7,6 +7,7 @@ use std::{ path::{Path, PathBuf}, sync::{LazyLock, OnceLock}, }; +use zed_extension_api::Os; pub fn versioned_gem_home( base_dir: &Path, @@ -34,19 +35,30 @@ pub fn versioned_gem_home( /// A simple wrapper around the `gem` command. pub struct Gemset { gem_home: PathBuf, + path_separator: char, envs: Vec<(String, String)>, cached_env: OnceLock>, command_executor: Box, } +fn path_list_separator(os: Os) -> char { + if matches!(os, Os::Windows) { + ';' + } else { + ':' + } +} + impl Gemset { pub fn new( gem_home: PathBuf, + os: Os, envs: Option<&[(&str, &str)]>, command_executor: Box, ) -> Self { Self { gem_home, + path_separator: path_list_separator(os), envs: envs.map_or(Vec::new(), |envs| { envs.iter() .map(|&(k, v)| (k.to_string(), v.to_string())) @@ -75,6 +87,7 @@ impl Gemset { .collect(); let gem_path = self.gem_home.display().to_string(); + let separator = self.path_separator; // If the GEM_PATH env variable is already set, // prepend our gem home directory to it to ensure @@ -84,11 +97,11 @@ impl Gemset { .and_modify(|existing_gem_path| { let gem_home_path = Path::new(&gem_path); let already_listed = existing_gem_path - .split(':') + .split(separator) .any(|entry| Path::new(entry) == gem_home_path); if !already_listed { - *existing_gem_path = format!("{gem_path}:{existing_gem_path}"); + *existing_gem_path = format!("{gem_path}{separator}{existing_gem_path}"); } }) .or_insert(gem_path); @@ -97,7 +110,7 @@ impl Gemset { env_map .entry("PATH".to_string()) .and_modify(|path| { - *path = format!("{}:{}", path, self.gem_home.join("bin").display()) + *path = format!("{path}{separator}{}", self.gem_home.join("bin").display()) }) .or_insert(self.gem_home.join("bin").display().to_string()); @@ -211,7 +224,12 @@ mod tests { const TEST_GEM_PATH: &str = "/test/gem_path"; fn create_gemset(envs: Option<&[(&str, &str)]>, mock_executor: MockCommandExecutor) -> Gemset { - Gemset::new(TEST_GEM_HOME.into(), envs, Box::new(mock_executor)) + Gemset::new( + TEST_GEM_HOME.into(), + Os::Linux, + envs, + Box::new(mock_executor), + ) } #[test] @@ -351,6 +369,7 @@ mod tests { fn test_gem_bin_path() { let gemset = Gemset::new( TEST_GEM_HOME.into(), + Os::Linux, None, Box::new(MockCommandExecutor::new()), ); @@ -367,6 +386,7 @@ mod tests { fn test_gem_env() { let gemset = Gemset::new( TEST_GEM_HOME.into(), + Os::Linux, Some(&[("GEM_PATH", TEST_GEM_PATH), ("PATH", "/usr/bin")]), Box::new(MockCommandExecutor::new()), ); @@ -388,6 +408,7 @@ mod tests { let existing = format!("{TEST_GEM_PATH}:{TEST_GEM_HOME}"); let gemset = Gemset::new( TEST_GEM_HOME.into(), + Os::Linux, Some(&[("GEM_PATH", &existing)]), Box::new(MockCommandExecutor::new()), ); @@ -400,6 +421,7 @@ mod tests { fn test_gem_env_without_gem_path() { let gemset = Gemset::new( TEST_GEM_HOME.into(), + Os::Linux, Some(&[("PATH", "/usr/bin")]), Box::new(MockCommandExecutor::new()), ); @@ -408,6 +430,48 @@ mod tests { assert_eq!(env.get("GEM_PATH").unwrap(), TEST_GEM_HOME); } + #[test] + fn test_gem_env_on_windows() { + let gem_home = "C:/zed/gems/abc"; + let gemset = Gemset::new( + gem_home.into(), + Os::Windows, + Some(&[ + ("GEM_PATH", "C:/Ruby34/lib/ruby/gems/3.4.0;C:/Users/x/.gem"), + ("PATH", "C:/Windows/System32"), + ]), + Box::new(MockCommandExecutor::new()), + ); + let env: std::collections::HashMap = gemset.env().iter().cloned().collect(); + + assert_eq!( + env.get("GEM_PATH").unwrap(), + "C:/zed/gems/abc;C:/Ruby34/lib/ruby/gems/3.4.0;C:/Users/x/.gem" + ); + assert_eq!( + env.get("PATH").unwrap(), + &format!( + "C:/Windows/System32;{}", + Path::new(gem_home).join("bin").display() + ) + ); + } + + #[test] + fn test_gem_env_on_windows_does_not_duplicate_gem_home_in_gem_path() { + let gem_home = "C:/zed/gems/abc"; + let existing = format!("C:/Ruby34/lib/ruby/gems/3.4.0;{gem_home}"); + let gemset = Gemset::new( + gem_home.into(), + Os::Windows, + Some(&[("GEM_PATH", &existing)]), + Box::new(MockCommandExecutor::new()), + ); + let env: std::collections::HashMap = gemset.env().iter().cloned().collect(); + + assert_eq!(env.get("GEM_PATH").unwrap(), &existing); + } + #[test] fn test_install_gem_success() { let mock_executor = MockCommandExecutor::new(); @@ -456,6 +520,7 @@ mod tests { ); let gemset = Gemset::new( TEST_GEM_HOME.into(), + Os::Linux, Some(&[("CUSTOM_VAR", "custom_value")]), Box::new(mock_executor), ); diff --git a/src/language_servers/language_server.rs b/src/language_servers/language_server.rs index 7706e7f..64e59a0 100644 --- a/src/language_servers/language_server.rs +++ b/src/language_servers/language_server.rs @@ -255,6 +255,7 @@ pub trait LanguageServer { let gemset = Gemset::new( gem_home, + zed::current_platform().0, Some(&worktree_shell_env_vars), Box::new(RealCommandExecutor), ); diff --git a/src/ruby.rs b/src/ruby.rs index e1acc65..9c79613 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -162,7 +162,12 @@ impl zed::Extension for RubyExtension { .map_err(|e| format!("Failed to get extension directory: {e:#}"))?; let gem_home = versioned_gem_home(&base_dir, &env_vars, &RealCommandExecutor) .map_err(|e| format!("{:#}", e))?; - let gemset = Gemset::new(gem_home, Some(&env_vars), Box::new(RealCommandExecutor)); + let gemset = Gemset::new( + gem_home, + zed::current_platform().0, + Some(&env_vars), + Box::new(RealCommandExecutor), + ); gemset .install_gem("debug") .map_err(|e| format!("Failed to install debug gem: {e:#}"))?;