From 3e6435eddc16ecb3398ab88acfbdcbe4e19a5b1d Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Tue, 3 Jun 2025 10:35:13 -0400 Subject: [PATCH] Fix Python virtual environment detection (#31934) # Fix Python Virtual Environment Detection in Zed ## Problem Zed was not properly detecting Python virtual environments when a project didn't contain a `pyrightconfig.json` file. This caused Pyright (the Python language server) to report `reportMissingImports` errors for packages installed in virtual environments, even though the virtual environment was correctly set up and worked fine in other editors. The issue was that while Zed's `PythonToolchainProvider` correctly detected virtual environments, this information wasn't being communicated to Pyright in a format it could understand. ## Root Cause The main issue was in how Zed communicated virtual environment configuration to Pyright through the Language Server Protocol (LSP). When Pyright requests workspace configuration, it expects virtual environment settings (`venvPath` and `venv`) at the root level of the configuration object - the same format used in `pyrightconfig.json` files. However, Zed was attempting to place these settings in various nested locations that Pyright wasn't checking. ## Solution The fix involves several coordinated changes to ensure Pyright receives virtual environment configuration in all the ways it might expect: ### 1. Enhanced Workspace Configuration (`workspace_configuration` method) - When a virtual environment is detected, Zed now sets `venvPath` and `venv` at the root level of the configuration object, matching the exact format of a `pyrightconfig.json` file - Uses relative path `"."` when the virtual environment is located in the workspace root - Also sets `python.pythonPath` and `python.defaultInterpreterPath` for compatibility with different Pyright versions ### 2. Environment Variables for All Language Server Binaries - Updated `check_if_user_installed`, `fetch_server_binary`, `check_if_version_installed`, and `cached_server_binary` methods to include shell environment variables - This ensures environment variables like `VIRTUAL_ENV` are available to Pyright, helping with automatic virtual environment detection ### 3. Initialization Options - Added minimal initialization options to enable Pyright's automatic path searching and import completion features - Sets `autoSearchPaths: true` and `useLibraryCodeForTypes: true` to improve Pyright's ability to find packages ## Key Changes The workspace configuration now properly formats virtual environment configuration: - Root level: `venvPath` and `venv` (matches pyrightconfig.json format) - Python section: `pythonPath` and `defaultInterpreterPath` for interpreter paths ## Impact - Users no longer need to create a `pyrightconfig.json` file for virtual environment detection - Python projects with virtual environments in standard locations (`.venv`, `venv`, etc.) will work out of the box - Import resolution for packages installed in virtual environments now works correctly - Maintains compatibility with manual `pyrightconfig.json` configuration for complex setups ## Testing The changes were tested with Python projects using virtual environments without `pyrightconfig.json` files. Pyright now correctly resolves imports from packages installed in the virtual environment, eliminating the `reportMissingImports` errors. ## Release Notes - Fixed Python virtual environment detection when no `pyrightconfig.json` is present - Pyright now correctly resolves imports from packages installed in virtual environments (`.venv`, `venv`, etc.) - Python projects with virtual environments no longer show false `reportMissingImports` errors - Improved Python development experience with automatic virtual environment configuration --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/languages/src/python.rs | 93 +++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a35608b473..b1f5706b69 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -106,6 +106,24 @@ impl LspAdapter for PythonLspAdapter { Self::SERVER_NAME.clone() } + async fn initialization_options( + self: Arc, + _: &dyn Fs, + _: &Arc, + ) -> Result> { + // Provide minimal initialization options + // Virtual environment configuration will be handled through workspace configuration + Ok(Some(json!({ + "python": { + "analysis": { + "autoSearchPaths": true, + "useLibraryCodeForTypes": true, + "autoImportCompletions": true + } + } + }))) + } + async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, @@ -128,9 +146,10 @@ impl LspAdapter for PythonLspAdapter { let path = node_modules_path.join(NODE_MODULE_RELATIVE_SERVER_PATH); + let env = delegate.shell_env().await; Some(LanguageServerBinary { path: node, - env: None, + env: Some(env), arguments: server_binary_arguments(&path), }) } @@ -151,7 +170,7 @@ impl LspAdapter for PythonLspAdapter { &self, latest_version: Box, container_dir: PathBuf, - _: &dyn LspAdapterDelegate, + delegate: &dyn LspAdapterDelegate, ) -> Result { let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); @@ -163,9 +182,10 @@ impl LspAdapter for PythonLspAdapter { ) .await?; + let env = delegate.shell_env().await; Ok(LanguageServerBinary { path: self.node.binary_path().await?, - env: None, + env: Some(env), arguments: server_binary_arguments(&server_path), }) } @@ -174,7 +194,7 @@ impl LspAdapter for PythonLspAdapter { &self, version: &(dyn 'static + Send + Any), container_dir: &PathBuf, - _: &dyn LspAdapterDelegate, + delegate: &dyn LspAdapterDelegate, ) -> Option { let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(SERVER_PATH); @@ -192,9 +212,10 @@ impl LspAdapter for PythonLspAdapter { if should_install_language_server { None } else { + let env = delegate.shell_env().await; Some(LanguageServerBinary { path: self.node.binary_path().await.ok()?, - env: None, + env: Some(env), arguments: server_binary_arguments(&server_path), }) } @@ -203,9 +224,11 @@ impl LspAdapter for PythonLspAdapter { async fn cached_server_binary( &self, container_dir: PathBuf, - _: &dyn LspAdapterDelegate, + delegate: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &self.node).await + let mut binary = get_cached_server_binary(container_dir, &self.node).await?; + binary.env = Some(delegate.shell_env().await); + Some(binary) } async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { @@ -308,22 +331,64 @@ impl LspAdapter for PythonLspAdapter { .and_then(|s| s.settings.clone()) .unwrap_or_default(); - // If python.pythonPath is not set in user config, do so using our toolchain picker. + // If we have a detected toolchain, configure Pyright to use it if let Some(toolchain) = toolchain { if user_settings.is_null() { user_settings = Value::Object(serde_json::Map::default()); } let object = user_settings.as_object_mut().unwrap(); - if let Some(python) = object + + let interpreter_path = toolchain.path.to_string(); + + // Detect if this is a virtual environment + if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { + if let Some(venv_dir) = interpreter_dir.parent() { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() + || venv_dir.join("bin/activate").exists() + || venv_dir.join("Scripts/activate.bat").exists() + { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } + + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); + } + } + } + } + + // Always set the python interpreter path + // Get or create the python section + let python = object .entry("python") .or_insert(Value::Object(serde_json::Map::default())) .as_object_mut() - { - python - .entry("pythonPath") - .or_insert(Value::String(toolchain.path.into())); - } + .unwrap(); + + // Set both pythonPath and defaultInterpreterPath for compatibility + python.insert( + "pythonPath".to_owned(), + Value::String(interpreter_path.clone()), + ); + python.insert( + "defaultInterpreterPath".to_owned(), + Value::String(interpreter_path), + ); } + user_settings }) }