diff --git a/assets/settings/default.json b/assets/settings/default.json index c4b15c37ec..4288c9be71 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -546,6 +546,14 @@ // "delay_ms": 600 } }, + // Configuration for how direnv configuration should be loaded. May take 2 values: + // 1. Load direnv configuration through the shell hook, works for POSIX shells and fish. + // "direnv": "shell_hook" + // 2. Load direnv configuration using `direnv export json` directly. + // This can help with some shells that otherwise would not detect + // the direnv environment, such as nushell or elvish. + // "direnv": "direct" + "direnv": "shell_hook", "inline_completions": { // A list of globs representing files that inline completions should be disabled for. "disabled_globs": [".env"] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d50c6233fb..15db5d3bb1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -73,7 +73,7 @@ use paths::{ }; use postage::watch; use prettier_support::{DefaultPrettier, PrettierInstance}; -use project_settings::{LspSettings, ProjectSettings}; +use project_settings::{DirenvSettings, LspSettings, ProjectSettings}; use rand::prelude::*; use rpc::{ErrorCode, ErrorExt as _}; use search::SearchQuery; @@ -11640,6 +11640,7 @@ pub struct ProjectLspAdapterDelegate { http_client: Arc, language_registry: Arc, shell_env: Mutex>>, + load_direnv: DirenvSettings, } impl ProjectLspAdapterDelegate { @@ -11648,6 +11649,7 @@ impl ProjectLspAdapterDelegate { worktree: &Model, cx: &ModelContext, ) -> Arc { + let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone(); Arc::new(Self { project: cx.weak_model(), worktree: worktree.read(cx).snapshot(), @@ -11655,12 +11657,13 @@ impl ProjectLspAdapterDelegate { http_client: project.client.http_client(), language_registry: project.languages.clone(), shell_env: Default::default(), + load_direnv, }) } async fn load_shell_env(&self) { let worktree_abs_path = self.worktree.abs_path(); - let shell_env = load_shell_environment(&worktree_abs_path) + let shell_env = load_shell_environment(&worktree_abs_path, &self.load_direnv) .await .with_context(|| { format!("failed to determine load login shell environment in {worktree_abs_path:?}") @@ -11874,7 +11877,44 @@ fn include_text(server: &lsp::LanguageServer) -> bool { .unwrap_or(false) } -async fn load_shell_environment(dir: &Path) -> Result> { +async fn load_direnv_environment(dir: &Path) -> Result>> { + let Ok(direnv_path) = which::which("direnv") else { + return Ok(None); + }; + + let direnv_output = smol::process::Command::new(direnv_path) + .args(["export", "json"]) + .current_dir(dir) + .output() + .await + .context("failed to spawn direnv to get local environment variables")?; + + anyhow::ensure!( + direnv_output.status.success(), + "direnv exited with error {:?}", + direnv_output.status + ); + + let output = String::from_utf8_lossy(&direnv_output.stdout); + if output.is_empty() { + return Ok(None); + } + + Ok(Some( + serde_json::from_str(&output).context("failed to parse direnv output")?, + )) +} + +async fn load_shell_environment( + dir: &Path, + load_direnv: &DirenvSettings, +) -> Result> { + let direnv_environment = match load_direnv { + DirenvSettings::ShellHook => None, + DirenvSettings::Direct => load_direnv_environment(dir).await?, + } + .unwrap_or(HashMap::default()); + let marker = "ZED_SHELL_START"; let shell = env::var("SHELL").context( "SHELL environment variable is not assigned so we can't source login environment variables", @@ -11885,6 +11925,11 @@ async fn load_shell_environment(dir: &Path) -> Result> { // `cd`'d into it. We do that because tools like direnv, asdf, ... // hook into `cd` and only set up the env after that. // + // If the user selects `Direct` for direnv, it would set an environment + // variable that later uses to know that it should not run the hook. + // We would include in `.envs` call so it is okay to run the hook + // even if direnv direct mode is enabled. + // // In certain shells we need to execute additional_command in order to // trigger the behavior of direnv, etc. // @@ -11912,6 +11957,7 @@ async fn load_shell_environment(dir: &Path) -> Result> { let output = smol::process::Command::new(&shell) .args(["-i", "-c", &command]) + .envs(direnv_environment) .output() .await .context("failed to spawn login shell to source login environment variables")?; diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index fdf43a7edd..ecb2b31cb0 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -20,6 +20,23 @@ pub struct ProjectSettings { /// Configuration for Git-related features #[serde(default)] pub git: GitSettings, + + /// Configuration for how direnv configuration should be loaded + #[serde(default)] + pub load_direnv: DirenvSettings, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DirenvSettings { + /// Load direnv configuration through a shell hook + #[default] + ShellHook, + /// Load direnv configuration directly using `direnv export json` + /// + /// Warning: This option is experimental and might cause some inconsistent behaviour compared to using the shell hook. + /// If it does, please report it to GitHub + Direct, } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b0e0375516..ff2c299328 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -190,6 +190,22 @@ You can also set other OpenType features, like setting `cv01` to `7`: The `left_padding` and `right_padding` options define the relative width of the left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.4`. +## Direnv Integration + +- Description: Settings for [direnv](https://direnv.net/) integration. Requires `direnv` to be installed. `direnv` integration currently only means that the environment variables set by a `direnv` configuration can be used to detect some language servers in `$PATH` instead of installing them. +- Setting: `direnv` +- Default: + +```json +"direnv": "shell_hook" +``` + +**Options** +There are two options to choose from: + +1. `shell_hook`: Use the shell hook to load direnv. This relies on direnv to activate upon entering the directory. Supports POSIX shells and fish. +2. `direct`: Use `direnv export json` to load direnv. This will load direnv directly without relying on the shell hook and might cause some inconsistencies. This allows direnv to work with any shell. + ## Inline Completions - Description: Settings for inline completions.