diff --git a/Cargo.toml b/Cargo.toml index bc7b4799b8..c3b443767a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,7 +132,6 @@ rattler_shell = { version = "0.22.22", default-features = false } rattler_solve = { version = "1.4.0", default-features = false } rattler_virtual_packages = { version = "2.0.6", default-features = false } - # Bumping this to a higher version breaks the Windows path handling. url = "2.5.4" uv-auth = { git = "https://github.com/astral-sh/uv", tag = "0.6.1" } diff --git a/crates/pixi_config/src/lib.rs b/crates/pixi_config/src/lib.rs index 7623bcaeff..b8176e8aa2 100644 --- a/crates/pixi_config/src/lib.rs +++ b/crates/pixi_config/src/lib.rs @@ -121,10 +121,14 @@ pub struct ConfigCliPrompt { #[arg(long)] change_ps1: Option, } + impl From for Config { fn from(cli: ConfigCliPrompt) -> Self { Self { - change_ps1: cli.change_ps1, + shell: ShellConfig { + change_ps1: cli.change_ps1, + ..Default::default() + }, ..Default::default() } } @@ -133,7 +137,7 @@ impl From for Config { impl ConfigCliPrompt { pub fn merge_config(self, config: Config) -> Config { let mut config = config; - config.change_ps1 = self.change_ps1.or(config.change_ps1); + config.shell.change_ps1 = self.change_ps1.or(config.shell.change_ps1); config } } @@ -181,12 +185,19 @@ pub struct ConfigCliActivation { /// Do not use the environment activation cache. (default: true except in experimental mode) #[arg(long)] force_activate: bool, + + /// Do not source the autocompletion scripts from the environment. + #[arg(long)] + no_completion: Option, } impl ConfigCliActivation { pub fn merge_config(self, config: Config) -> Config { let mut config = config; - config.force_activate = Some(self.force_activate); + config.shell.force_activate = Some(self.force_activate); + if let Some(no_completion) = self.no_completion { + config.shell.source_completion_scripts = Some(!no_completion); + } config } } @@ -194,7 +205,11 @@ impl ConfigCliActivation { impl From for Config { fn from(cli: ConfigCliActivation) -> Self { Self { - force_activate: Some(cli.force_activate), + shell: ShellConfig { + force_activate: Some(cli.force_activate), + source_completion_scripts: None, + change_ps1: None, + }, ..Default::default() } } @@ -565,13 +580,6 @@ pub struct Config { #[serde(skip_serializing_if = "Vec::is_empty")] pub default_channels: Vec, - /// If set to true, pixi will set the PS1 environment variable to a custom - /// value. - #[serde(default)] - #[serde(alias = "change_ps1")] // BREAK: remove to stop supporting snake_case alias - #[serde(skip_serializing_if = "Option::is_none")] - pub change_ps1: Option, - /// Path to the file containing the authentication token. #[serde(default)] #[serde(alias = "authentication_override_file")] // BREAK: remove to stop supporting snake_case alias @@ -623,10 +631,10 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub detached_environments: Option, - /// The option to disable the environment activation cache + /// Shell-specific configuration #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub force_activate: Option, + #[serde(skip_serializing_if = "ShellConfig::is_default")] + pub shell: ShellConfig, /// Experimental features that can be enabled. #[serde(default)] @@ -637,13 +645,24 @@ pub struct Config { #[serde(default)] #[serde(skip_serializing_if = "ConcurrencyConfig::is_default")] pub concurrency: ConcurrencyConfig, + + ////////////////////// + // Deprecated fields // + ////////////////////// + #[serde(default)] + #[serde(alias = "change_ps1")] // BREAK: remove to stop supporting snake_case alias + #[serde(skip_serializing_if = "Option::is_none")] + pub change_ps1: Option, + + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub force_activate: Option, } impl Default for Config { fn default() -> Self { Self { default_channels: Vec::new(), - change_ps1: None, authentication_override_file: None, tls_no_verify: None, mirrors: HashMap::new(), @@ -654,9 +673,13 @@ impl Default for Config { s3_options: HashMap::new(), detached_environments: None, pinning_strategy: None, - force_activate: None, + shell: ShellConfig::default(), experimental: ExperimentalConfig::default(), concurrency: ConcurrencyConfig::default(), + + // Deprecated fields + change_ps1: None, + force_activate: None, } } } @@ -713,6 +736,48 @@ impl From<&Config> for rattler_repodata_gateway::ChannelConfig { } } +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct ShellConfig { + /// The option to disable the environment activation cache + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub force_activate: Option, + + /// Whether to source completion scripts from the environment or not. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + source_completion_scripts: Option, + + /// If set to true, pixi will set the PS1 environment variable to a custom + /// value. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub change_ps1: Option, +} + +impl ShellConfig { + pub fn merge(self, other: Self) -> Self { + Self { + force_activate: other.force_activate.or(self.force_activate), + source_completion_scripts: other + .source_completion_scripts + .or(self.source_completion_scripts), + change_ps1: other.change_ps1.or(self.change_ps1), + } + } + + pub fn is_default(&self) -> bool { + self.force_activate.is_none() + && self.source_completion_scripts.is_none() + && self.change_ps1.is_none() + } + + pub fn source_completion_scripts(&self) -> bool { + self.source_completion_scripts.unwrap_or(true) + } +} + #[derive(thiserror::Error, Debug)] pub enum ConfigError { #[error("no file was found at {0}")] @@ -753,16 +818,42 @@ impl Config { /// /// Parsing errors #[inline] - pub fn from_toml(toml: &str) -> miette::Result<(Config, Set)> { + pub fn from_toml( + toml: &str, + source_path: Option<&Path>, + ) -> miette::Result<(Config, Set)> { let de = toml_edit::de::Deserializer::from_str(toml).into_diagnostic()?; // Deserialize the config and collect unused keys let mut unused_keys = Set::new(); - let config: Config = serde_ignored::deserialize(de, |path| { + let mut config: Config = serde_ignored::deserialize(de, |path| { unused_keys.insert(path.to_string()); }) .into_diagnostic()?; + fn create_deprecation_warning(old: &str, new: &str, source_path: Option<&Path>) { + let msg = format!( + "Please replace '{}' with '{}', the field is deprecated and will be removed in a future release.", + console::style(old).red(), console::style(new).green() + ); + match source_path { + Some(path) => { + tracing::warn!("In '{}': {}", console::style(path.display()).bold(), msg,) + } + None => tracing::warn!("{}", msg), + } + } + + if config.change_ps1.is_some() { + create_deprecation_warning("change-ps1", "shell.change-ps1", source_path); + config.shell.change_ps1 = config.change_ps1; + } + + if config.force_activate.is_some() { + create_deprecation_warning("force-activate", "shell.force-activate", source_path); + config.shell.force_activate = config.force_activate; + } + Ok((config, unused_keys)) } @@ -788,8 +879,8 @@ impl Config { Err(e) => return Err(ConfigError::ReadError(e)), }; - let (mut config, unused_keys) = - Config::from_toml(&s).map_err(|e| ConfigError::ParseError(e, path.to_path_buf()))?; + let (mut config, unused_keys) = Config::from_toml(&s, Some(path)) + .map_err(|e| ConfigError::ParseError(e, path.to_path_buf()))?; if !unused_keys.is_empty() { tracing::warn!( @@ -926,7 +1017,6 @@ impl Config { pub fn get_keys(&self) -> &[&str] { &[ "default-channels", - "change-ps1", "authentication-override-file", "tls-no-verify", "mirrors", @@ -942,6 +1032,10 @@ impl Config { "pypi-config.index-url", "pypi-config.extra-index-urls", "pypi-config.keyring-provider", + "shell", + "shell.force-activate", + "shell.source-completion-scripts", + "shell.change-ps1", "s3-options", "s3-options.", "s3-options..endpoint-url", @@ -965,7 +1059,6 @@ impl Config { other.default_channels }, tls_no_verify: other.tls_no_verify.or(self.tls_no_verify), - change_ps1: other.change_ps1.or(self.change_ps1), authentication_override_file: other .authentication_override_file .or(self.authentication_override_file), @@ -984,10 +1077,14 @@ impl Config { }, detached_environments: other.detached_environments.or(self.detached_environments), pinning_strategy: other.pinning_strategy.or(self.pinning_strategy), - force_activate: other.force_activate, + shell: self.shell.merge(other.shell), experimental: self.experimental.merge(other.experimental), // Make other take precedence over self to allow for setting the value through the CLI concurrency: self.concurrency.merge(other.concurrency), + + // Deprecated fields that we can ignore as we handle them inside `shell.` field + change_ps1: None, + force_activate: None, } } @@ -1008,7 +1105,7 @@ impl Config { /// Retrieve the value for the change_ps1 field (defaults to true). pub fn change_ps1(&self) -> bool { - self.change_ps1.unwrap_or(true) + self.shell.change_ps1.unwrap_or(true) } /// Retrieve the value for the auth_file field. @@ -1043,7 +1140,7 @@ impl Config { } pub fn force_activate(&self) -> bool { - self.force_activate.unwrap_or(false) + self.shell.force_activate.unwrap_or(false) } pub fn experimental_activation_cache_usage(&self) -> bool { @@ -1082,9 +1179,6 @@ impl Config { .into_diagnostic()? .unwrap_or_default(); } - "change-ps1" => { - self.change_ps1 = value.map(|v| v.parse()).transpose().into_diagnostic()?; - } "authentication-override-file" => { self.authentication_override_file = value.map(PathBuf::from); } @@ -1111,6 +1205,12 @@ impl Config { .transpose() .into_diagnostic()? } + "change-ps1" => { + return Err(miette::miette!("The `change-ps1` field is deprecated. Please use the `shell.change-ps1` field instead.")); + } + "force-activate" => { + return Err(miette::miette!("The `force-activate` field is deprecated. Please use the `shell.force-activate` field instead.")); + } key if key.starts_with("repodata-config") => { if key == "repodata-config" { self.repodata_config = value @@ -1293,6 +1393,34 @@ impl Config { _ => return Err(err), } } + key if key.starts_with("shell") => { + if key == "shell" { + if let Some(value) = value { + self.shell = serde_json::de::from_str(&value).into_diagnostic()?; + } else { + self.shell = ShellConfig::default(); + } + return Ok(()); + } else if !key.starts_with("shell.") { + return Err(err); + } + let subkey = key.strip_prefix("shell.").unwrap(); + match subkey { + "force-activate" => { + self.shell.force_activate = + value.map(|v| v.parse()).transpose().into_diagnostic()?; + } + "source-completion-scripts" => { + self.shell.source_completion_scripts = + value.map(|v| v.parse()).transpose().into_diagnostic()?; + } + "change-ps1" => { + self.shell.change_ps1 = + value.map(|v| v.parse()).transpose().into_diagnostic()?; + } + _ => return Err(err), + } + } _ => return Err(err), } @@ -1392,7 +1520,7 @@ UNUSED = "unused" "#, env!("CARGO_MANIFEST_DIR").replace('\\', "\\\\").as_str() ); - let (config, unused) = Config::from_toml(toml.as_str()).unwrap(); + let (config, unused) = Config::from_toml(toml.as_str(), None).unwrap(); assert_eq!( config.default_channels, vec![NamedChannelOrUrl::from_str("conda-forge").unwrap()] @@ -1406,7 +1534,7 @@ UNUSED = "unused" assert!(unused.contains("UNUSED")); let toml = r"detached-environments = true"; - let (config, _) = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml, None).unwrap(); assert_eq!( config.detached_environments().path().unwrap().unwrap(), get_cache_dir() @@ -1425,7 +1553,7 @@ UNUSED = "unused" #[case("no-pin", PinningStrategy::NoPin)] fn test_config_parse_pinning_strategy(#[case] input: &str, #[case] expected: PinningStrategy) { let toml = format!("pinning-strategy = \"{}\"", input); - let (config, _) = Config::from_toml(&toml).unwrap(); + let (config, _) = Config::from_toml(&toml, None).unwrap(); assert_eq!(config.pinning_strategy, Some(expected)); } @@ -1470,12 +1598,12 @@ UNUSED = "unused" extra-index-urls = ["https://pypi.org/simple2"] keyring-provider = "subprocess" "#; - let (config, _) = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml, None).unwrap(); assert_eq!( config.pypi_config().index_url, Some(Url::parse("https://pypi.org/simple").unwrap()) ); - assert!(config.pypi_config().extra_index_urls.len() == 1); + assert_eq!(config.pypi_config().extra_index_urls.len(), 1); assert_eq!( config.pypi_config().keyring_provider, Some(KeyringProvider::Subprocess) @@ -1491,7 +1619,7 @@ UNUSED = "unused" keyring-provider = "subprocess" allow-insecure-host = ["https://localhost:1234", "*"] "#; - let (config, _) = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml, None).unwrap(); assert_eq!( config.pypi_config().allow_insecure_host, vec!["https://localhost:1234", "*",] @@ -1506,7 +1634,7 @@ UNUSED = "unused" region = "us-east-1" force-path-style = false "#; - let (config, _) = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml, None).unwrap(); let s3_options = config.s3_options; assert_eq!( s3_options["bucket1"].endpoint_url, @@ -1524,7 +1652,7 @@ UNUSED = "unused" region = "us-east-1" # force-path-style = false "#; - let result = Config::from_toml(toml); + let result = Config::from_toml(toml, None); assert!(result.is_err()); assert!(result .err() @@ -1554,7 +1682,6 @@ UNUSED = "unused" solves: 5, ..ConcurrencyConfig::default() }, - change_ps1: Some(false), authentication_override_file: Some(PathBuf::default()), mirrors: HashMap::from([( Url::parse("https://conda.anaconda.org/conda-forge").unwrap(), @@ -1565,7 +1692,11 @@ UNUSED = "unused" use_environment_activation_cache: Some(true), }, loaded_from: Vec::from([PathBuf::from_str("test").unwrap()]), - force_activate: Some(true), + shell: ShellConfig { + force_activate: Some(true), + source_completion_scripts: None, + change_ps1: Some(false), + }, pypi_config: PyPIConfig { allow_insecure_host: Vec::from(["test".to_string()]), extra_index_urls: Vec::from([ @@ -1594,6 +1725,9 @@ UNUSED = "unused" RepodataChannelConfig::default(), )]), }, + // Deprecated keys + change_ps1: None, + force_activate: None, }; let original_other = other.clone(); config = config.merge_config(other); @@ -1718,7 +1852,7 @@ UNUSED = "unused" disable_bzip2 = true disable_zstd = true "#; - let (config, _) = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml, None).unwrap(); assert_eq!( config.default_channels, vec![NamedChannelOrUrl::from_str("conda-forge").unwrap()] @@ -1756,7 +1890,7 @@ UNUSED = "unused" disable-zstd = true disable-sharded = true "#; - Config::from_toml(toml).unwrap(); + Config::from_toml(toml, None).unwrap(); } #[test] @@ -1851,7 +1985,11 @@ UNUSED = "unused" Some(KeyringProvider::Subprocess) ); - config.set("change-ps1", None).unwrap(); + let deprecated = config.set("change-ps1", None); + assert!(deprecated.is_err()); + assert!(deprecated.unwrap_err().to_string().contains("deprecated")); + + config.set("shell.change-ps1", None).unwrap(); assert_eq!(config.change_ps1, None); config @@ -1981,7 +2119,7 @@ UNUSED = "unused" disable-bzip2 = false disable-zstd = false "#; - let (config, _) = Config::from_toml(toml).unwrap(); + let (config, _) = Config::from_toml(toml, None).unwrap(); let repodata_config = config.repodata_config(); assert_eq!(repodata_config.default.disable_jlap, Some(true)); assert_eq!(repodata_config.default.disable_bzip2, Some(true)); diff --git a/crates/pixi_config/src/snapshots/pixi_config__tests__config_merge_multiple.snap b/crates/pixi_config/src/snapshots/pixi_config__tests__config_merge_multiple.snap index 823d2bcad4..cc7430ecd2 100644 --- a/crates/pixi_config/src/snapshots/pixi_config__tests__config_merge_multiple.snap +++ b/crates/pixi_config/src/snapshots/pixi_config__tests__config_merge_multiple.snap @@ -1,6 +1,5 @@ --- source: crates/pixi_config/src/lib.rs -assertion_line: 1698 expression: debug --- Config { @@ -15,9 +14,6 @@ Config { "defaults", ), ], - change_ps1: Some( - true, - ), authentication_override_file: None, tls_no_verify: Some( false, @@ -123,7 +119,13 @@ Config { true, ), ), - force_activate: None, + shell: ShellConfig { + force_activate: None, + source_completion_scripts: None, + change_ps1: Some( + true, + ), + }, experimental: ExperimentalConfig { use_environment_activation_cache: None, }, @@ -131,4 +133,6 @@ Config { solves: 1, downloads: 50, }, + change_ps1: None, + force_activate: None, } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f3bc65a32b..35946dada6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -754,6 +754,7 @@ To exit the pixi shell, simply run `exit`. - `--revalidate`: Revalidate the full environment, instead of checking lock file hash. [more info](../features/environment.md#environment-installation-metadata) - `--concurrent-downloads`: The number of concurrent downloads to use when installing packages. Defaults to 50. - `--concurrent-solves`: The number of concurrent solves to use when installing packages. Defaults to the number of cpu threads. +- `--no-completion`: Do not source the autocompletion scripts from the environment. ```shell pixi shell @@ -786,6 +787,7 @@ This command prints the activation script of an environment. - `--revalidate`: Revalidate the full environment, instead of checking lock file hash. [more info](../features/environment.md#environment-installation-metadata) - `--concurrent-downloads`: The number of concurrent downloads to use when installing packages. Defaults to 50. - `--concurrent-solves`: The number of concurrent solves to use when installing packages. Defaults to the number of cpu threads. +- `--no-completion`: Do not source the autocompletion scripts from the environment. ```shell pixi shell-hook diff --git a/docs/reference/pixi_configuration.md b/docs/reference/pixi_configuration.md index 568fb28fcd..f638d2cb27 100644 --- a/docs/reference/pixi_configuration.md +++ b/docs/reference/pixi_configuration.md @@ -73,14 +73,17 @@ This defaults to only conda-forge. !!! note The `default-channels` are only used when initializing a new project. Once initialized the `channels` are used from the project manifest. -### `change-ps1` +### `shell` -When set to false, the `(pixi)` prefix in the shell prompt is removed. -This applies to the `pixi shell` subcommand. -You can override this from the CLI with `--change-ps1`. +- `change-ps1`: When set to `false`, the `(pixi)` prefix in the shell prompt is removed. + This applies to the `pixi shell` subcommand. + You can override this from the CLI with `--change-ps1`. +- `force-activate`: When set to `true` the re-activation of the environment will always happen. +This is used in combination with the [`experimental`](#experimental) feature `use-environment-activation-cache`. +- `source-completion-scripts`: When set to `false`, pixi will not source the autocompletion scripts of the environment when going into the shell. ```toml title="config.toml" ---8<-- "docs/source_files/pixi_config_tomls/main_config.toml:change-ps1" +--8<-- "docs/source_files/pixi_config_tomls/main_config.toml:shell" ``` ### `tls-no-verify` diff --git a/docs/source_files/pixi_config_tomls/main_config.toml b/docs/source_files/pixi_config_tomls/main_config.toml index e78c76149f..2bd1bf06f5 100644 --- a/docs/source_files/pixi_config_tomls/main_config.toml +++ b/docs/source_files/pixi_config_tomls/main_config.toml @@ -3,9 +3,6 @@ default-channels = ["conda-forge"] # --8<-- [end:default-channels] -# --8<-- [start:change-ps1] -change-ps1 = true -# --8<-- [end:change-ps1] # --8<-- [start:tls-no-verify] tls-no-verify = false @@ -90,3 +87,10 @@ use-environment-activation-cache = true "https://prefix.dev/bioconda", ] # --8<-- [end:mirrors] + +# --8<-- [start:shell] +[shell] +change-ps1 = false +force-activate = true +source-completion-scripts = false +# --8<-- [end:shell] diff --git a/src/cli/config.rs b/src/cli/config.rs index db99b1a497..3d5ffb8d42 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -328,7 +328,7 @@ fn partial_config(config: &mut Config, key: &str) -> miette::Result<()> { match key { "default-channels" => new.default_channels = config.default_channels.clone(), - "change-ps1" => new.change_ps1 = config.change_ps1, + "shell" => new.shell = config.shell.clone(), "tls-no-verify" => new.tls_no_verify = config.tls_no_verify, "authentication-override-file" => { new.authentication_override_file = config.authentication_override_file.clone() @@ -339,7 +339,6 @@ fn partial_config(config: &mut Config, key: &str) -> miette::Result<()> { _ => { let keys = [ "default-channels", - "change-ps1", "tls-no-verify", "authentication-override-file", "mirrors", diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 69be713d07..2247ed0cbe 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -19,6 +19,9 @@ use pixi_config::{ConfigCliActivation, ConfigCliPrompt}; #[cfg(target_family = "unix")] use pixi_pty::unix::PtySession; +#[cfg(target_family = "unix")] +use crate::prefix::Prefix; + /// Start a shell in the pixi environment of the project #[derive(Parser, Debug)] pub struct Args { @@ -139,6 +142,8 @@ async fn start_unix_shell( args: Vec<&str>, env: &HashMap, prompt: String, + prefix: &Prefix, + source_shell_completions: bool, ) -> miette::Result> { // create a tempfile for activation let mut temp_file = tempfile::Builder::new() @@ -153,6 +158,13 @@ async fn start_unix_shell( shell_script.set_env_var(key, value).into_diagnostic()?; } + if source_shell_completions { + if let Some(completions_dir) = shell.completion_script_location() { + shell_script + .source_completions(&prefix.root().join(completions_dir)) + .into_diagnostic()?; + } + } const DONE_STR: &str = "=== DONE ==="; shell_script.echo(DONE_STR).into_diagnostic()?; @@ -254,7 +266,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { let environment = workspace.environment_from_name_or_env_var(args.environment)?; // Make sure environment is up-to-date, default to install, users can avoid this with frozen or locked. - let (lock_file_data, _prefix) = get_update_lock_file_and_prefix( + #[allow(unused_variables)] + let (lock_file_data, prefix) = get_update_lock_file_and_prefix( &environment, UpdateMode::QuickValidate, UpdateLockFileOptions { @@ -309,15 +322,58 @@ pub async fn execute(args: Args) -> miette::Result<()> { }; #[cfg(target_family = "unix")] - let res = match interactive_shell { - ShellEnum::NuShell(nushell) => start_nu_shell(nushell, env, prompt_hook).await, - ShellEnum::PowerShell(pwsh) => start_powershell(pwsh, env, prompt_hook), - ShellEnum::Bash(bash) => start_unix_shell(bash, vec!["-l", "-i"], env, prompt_hook).await, - ShellEnum::Zsh(zsh) => start_unix_shell(zsh, vec!["-l", "-i"], env, prompt_hook).await, - ShellEnum::Fish(fish) => start_unix_shell(fish, vec![], env, prompt_hook).await, - ShellEnum::Xonsh(xonsh) => start_unix_shell(xonsh, vec![], env, prompt_hook).await, - _ => { - miette::bail!("Unsupported shell: {:?}", interactive_shell) + let res = { + let source_shell_completions = workspace.config().shell.source_completion_scripts(); + match interactive_shell { + ShellEnum::NuShell(nushell) => start_nu_shell(nushell, env, prompt_hook).await, + ShellEnum::PowerShell(pwsh) => start_powershell(pwsh, env, prompt_hook), + ShellEnum::Bash(bash) => { + start_unix_shell( + bash, + vec!["-l", "-i"], + env, + prompt_hook, + &prefix, + source_shell_completions, + ) + .await + } + ShellEnum::Zsh(zsh) => { + start_unix_shell( + zsh, + vec!["-l", "-i"], + env, + prompt_hook, + &prefix, + source_shell_completions, + ) + .await + } + ShellEnum::Fish(fish) => { + start_unix_shell( + fish, + vec![], + env, + prompt_hook, + &prefix, + source_shell_completions, + ) + .await + } + ShellEnum::Xonsh(xonsh) => { + start_unix_shell( + xonsh, + vec![], + env, + prompt_hook, + &prefix, + source_shell_completions, + ) + .await + } + _ => { + miette::bail!("Unsupported shell: {:?}", interactive_shell) + } } }; diff --git a/src/cli/shell_hook.rs b/src/cli/shell_hook.rs index a1391b98d7..7f3c921066 100644 --- a/src/cli/shell_hook.rs +++ b/src/cli/shell_hook.rs @@ -6,7 +6,7 @@ use pixi_config::{ConfigCliActivation, ConfigCliPrompt}; use rattler_lock::LockFile; use rattler_shell::{ activation::{ActivationVariables, PathModificationBehavior}, - shell::ShellEnum, + shell::{Shell, ShellEnum}, }; use serde::Serialize; use serde_json; @@ -79,7 +79,7 @@ async fn generate_activation_script( // If we are in a conda environment, we need to deactivate it before activating // the host / build prefix let conda_prefix = std::env::var("CONDA_PREFIX").ok().map(|p| p.into()); - let result = activator + let mut result = activator .activation(ActivationVariables { conda_prefix, path, @@ -87,6 +87,15 @@ async fn generate_activation_script( }) .into_diagnostic()?; + if project.config().shell.source_completion_scripts() { + if let Some(completions_dir) = shell.completion_script_location() { + result + .script + .source_completions(&environment.dir().join(completions_dir)) + .into_diagnostic()?; + } + } + let script = result.script.contents().into_diagnostic()?; let hook = prompt::shell_hook(&shell).unwrap_or_default().to_owned(); diff --git a/tests/integration_python/conftest.py b/tests/integration_python/conftest.py index 800cce3776..7d0bf87ace 100644 --- a/tests/integration_python/conftest.py +++ b/tests/integration_python/conftest.py @@ -23,7 +23,7 @@ def tmp_pixi_workspace(tmp_path: Path) -> Path: pixi_config = """ # Reset to defaults default-channels = ["conda-forge"] -change-ps1 = true +shell.change-ps1 = true tls-no-verify = false detached-environments = false pinning-strategy = "semver" diff --git a/tests/integration_python/test_main_cli.py b/tests/integration_python/test_main_cli.py index 77b39a477b..9c28f43c11 100644 --- a/tests/integration_python/test_main_cli.py +++ b/tests/integration_python/test_main_cli.py @@ -1159,6 +1159,39 @@ def test_dont_error_on_missing_platform(pixi: Path, tmp_pixi_workspace: Path) -> ) +def test_shell_hook_autocompletion(pixi: Path, tmp_pixi_workspace: Path) -> None: + manifest = tmp_pixi_workspace.joinpath("pixi.toml") + toml = f""" + {EMPTY_BOILERPLATE_PROJECT} + """ + manifest.write_text(toml) + + bash_comp_dir = ".pixi/envs/default/share/bash-completion/completions" + tmp_pixi_workspace.joinpath(bash_comp_dir).mkdir(parents=True, exist_ok=True) + tmp_pixi_workspace.joinpath(bash_comp_dir, "pixi.sh").touch() + verify_cli_command( + [pixi, "shell-hook", "--manifest-path", manifest, "--shell", "bash"], + stdout_contains=["source", "share/bash-completion/completions"], + ) + + zsh_comp_dir = ".pixi/envs/default/share/zsh/site-functions" + tmp_pixi_workspace.joinpath(zsh_comp_dir).mkdir(parents=True, exist_ok=True) + tmp_pixi_workspace.joinpath(zsh_comp_dir, "_pixi").touch() + verify_cli_command( + [pixi, "shell-hook", "--manifest-path", manifest, "--shell", "zsh"], + stdout_contains=["fpath+=", "share/zsh/site-functions", "autoload -Uz compinit"], + ) + + fish_comp_dir = ".pixi/envs/default/share/fish/vendor_completions.d" + tmp_pixi_workspace.joinpath(fish_comp_dir).mkdir(parents=True, exist_ok=True) + tmp_pixi_workspace.joinpath(fish_comp_dir, "pixi.fish").touch() + + verify_cli_command( + [pixi, "shell-hook", "--manifest-path", manifest, "--shell", "fish"], + stdout_contains=["for file in", "source", "share/fish/vendor_completions.d"], + ) + + def test_pixi_info_tasks(pixi: Path, tmp_pixi_workspace: Path) -> None: manifest = tmp_pixi_workspace.joinpath("pixi.toml") toml = """ diff --git a/tests/integration_rust/project_tests.rs b/tests/integration_rust/project_tests.rs index 850425dad9..3c6e648978 100644 --- a/tests/integration_rust/project_tests.rs +++ b/tests/integration_rust/project_tests.rs @@ -157,7 +157,7 @@ fn parse_valid_docs_configs() { let path = entry.path(); if path.extension().map(|ext| ext == "toml").unwrap_or(false) { let toml = fs_err::read_to_string(&path).unwrap(); - let (_config, unused_keys) = Config::from_toml(&toml).unwrap(); + let (_config, unused_keys) = Config::from_toml(&toml, None).unwrap(); assert_eq!( unused_keys, BTreeSet::::new(),