From da27f0e28fac85115a6a5f10a52a4c50aa419c69 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:45:51 -0800 Subject: [PATCH] Move zsh code out of bash folder Fixes #1148 --- .../terminal/shells/bash/bashConstants.ts | 2 - src/features/terminal/shells/bash/bashEnvs.ts | 47 +----- .../terminal/shells/bash/bashStartup.ts | 77 +-------- src/features/terminal/shells/providers.ts | 6 +- .../terminal/shells/zsh/zshConstants.ts | 3 + src/features/terminal/shells/zsh/zshEnvs.ts | 52 ++++++ .../terminal/shells/zsh/zshStartup.ts | 155 ++++++++++++++++++ 7 files changed, 216 insertions(+), 126 deletions(-) create mode 100644 src/features/terminal/shells/zsh/zshConstants.ts create mode 100644 src/features/terminal/shells/zsh/zshEnvs.ts create mode 100644 src/features/terminal/shells/zsh/zshStartup.ts diff --git a/src/features/terminal/shells/bash/bashConstants.ts b/src/features/terminal/shells/bash/bashConstants.ts index 72c838e8..5eb1a022 100644 --- a/src/features/terminal/shells/bash/bashConstants.ts +++ b/src/features/terminal/shells/bash/bashConstants.ts @@ -1,5 +1,3 @@ export const BASH_ENV_KEY = 'VSCODE_PYTHON_BASH_ACTIVATE'; -export const ZSH_ENV_KEY = 'VSCODE_PYTHON_ZSH_ACTIVATE'; export const BASH_OLD_ENV_KEY = 'VSCODE_BASH_ACTIVATE'; -export const ZSH_OLD_ENV_KEY = 'VSCODE_ZSH_ACTIVATE'; export const BASH_SCRIPT_VERSION = '0.1.1'; diff --git a/src/features/terminal/shells/bash/bashEnvs.ts b/src/features/terminal/shells/bash/bashEnvs.ts index 08219b4d..594e3972 100644 --- a/src/features/terminal/shells/bash/bashEnvs.ts +++ b/src/features/terminal/shells/bash/bashEnvs.ts @@ -1,10 +1,9 @@ import { EnvironmentVariableCollection } from 'vscode'; import { PythonEnvironment } from '../../../../api'; import { traceError } from '../../../../common/logging'; -import { ShellConstants } from '../../../common/shellConstants'; import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; import { ShellEnvsProvider } from '../startupProvider'; -import { BASH_ENV_KEY, ZSH_ENV_KEY } from './bashConstants'; +import { BASH_ENV_KEY } from './bashConstants'; export class BashEnvsProvider implements ShellEnvsProvider { constructor(public readonly shellType: 'bash' | 'gitbash') {} @@ -50,47 +49,3 @@ export class BashEnvsProvider implements ShellEnvsProvider { } } } - -export class ZshEnvsProvider implements ShellEnvsProvider { - public readonly shellType: string = ShellConstants.ZSH; - updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { - try { - const zshActivation = getShellActivationCommand(this.shellType, env); - if (zshActivation) { - const command = getShellCommandAsString(this.shellType, zshActivation); - const v = collection.get(ZSH_ENV_KEY); - if (v?.value === command) { - return; - } - collection.replace(ZSH_ENV_KEY, command); - } else { - collection.delete(ZSH_ENV_KEY); - } - } catch (err) { - traceError('Failed to update env variables for zsh', err); - collection.delete(ZSH_ENV_KEY); - } - } - - removeEnvVariables(collection: EnvironmentVariableCollection): void { - collection.delete(ZSH_ENV_KEY); - } - - getEnvVariables(env?: PythonEnvironment): Map | undefined { - if (!env) { - return new Map([[ZSH_ENV_KEY, undefined]]); - } - - try { - const zshActivation = getShellActivationCommand(this.shellType, env); - if (zshActivation) { - const command = getShellCommandAsString(this.shellType, zshActivation); - return new Map([[ZSH_ENV_KEY, command]]); - } - return undefined; - } catch (err) { - traceError('Failed to get env variables for zsh', err); - return undefined; - } - } -} diff --git a/src/features/terminal/shells/bash/bashStartup.ts b/src/features/terminal/shells/bash/bashStartup.ts index bbfb198c..86feb556 100644 --- a/src/features/terminal/shells/bash/bashStartup.ts +++ b/src/features/terminal/shells/bash/bashStartup.ts @@ -6,18 +6,13 @@ import { traceError, traceInfo, traceVerbose } from '../../../../common/logging' import { ShellConstants } from '../../../common/shellConstants'; import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; -import { BASH_ENV_KEY, BASH_OLD_ENV_KEY, BASH_SCRIPT_VERSION, ZSH_ENV_KEY, ZSH_OLD_ENV_KEY } from './bashConstants'; +import { BASH_ENV_KEY, BASH_OLD_ENV_KEY, BASH_SCRIPT_VERSION } from './bashConstants'; async function isBashLikeInstalled(): Promise { const result = await Promise.all([which('bash', { nothrow: true }), which('sh', { nothrow: true })]); return result.some((r) => r !== null); } -async function isZshInstalled(): Promise { - const result = await which('zsh', { nothrow: true }); - return result !== null; -} - async function isGitBashInstalled(): Promise { const gitPath = await which('git', { nothrow: true }); if (gitPath) { @@ -34,14 +29,6 @@ async function getBashProfiles(): Promise { return profile; } -async function getZshProfiles(): Promise { - const zdotdir = process.env.ZDOTDIR; - const baseDir = zdotdir || os.homedir(); - const profile: string = path.join(baseDir, '.zshrc'); - - return profile; -} - const regionStart = '# >>> vscode python'; const regionEnd = '# <<< vscode python'; @@ -180,68 +167,6 @@ export class BashStartupProvider implements ShellStartupScriptProvider { } } -export class ZshStartupProvider implements ShellStartupScriptProvider { - public readonly name: string = 'zsh'; - public readonly shellType: string = ShellConstants.ZSH; - - private async checkShellInstalled(): Promise { - const found = await isZshInstalled(); - if (!found) { - traceInfo('`zsh` was not found on the system', 'If it is installed make sure it is available on `PATH`'); - } - return found; - } - - async isSetup(): Promise { - const found = await this.checkShellInstalled(); - if (!found) { - return ShellSetupState.NotInstalled; - } - - try { - const zshProfiles = await getZshProfiles(); - return await isStartupSetup(zshProfiles, ZSH_ENV_KEY); - } catch (err) { - traceError('Failed to check zsh startup scripts', err); - return ShellSetupState.NotSetup; - } - } - - async setupScripts(): Promise { - const found = await this.checkShellInstalled(); - if (!found) { - return ShellScriptEditState.NotInstalled; - } - try { - const zshProfiles = await getZshProfiles(); - const result = await setupStartup(zshProfiles, ZSH_ENV_KEY, this.name); - return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; - } catch (err) { - traceError('Failed to setup zsh startup scripts', err); - return ShellScriptEditState.NotEdited; - } - } - - async teardownScripts(): Promise { - const found = await this.checkShellInstalled(); - if (!found) { - return ShellScriptEditState.NotInstalled; - } - try { - const zshProfiles = await getZshProfiles(); - await removeStartup(zshProfiles, ZSH_OLD_ENV_KEY); - const result = await removeStartup(zshProfiles, ZSH_ENV_KEY); - return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; - } catch (err) { - traceError('Failed to teardown zsh startup scripts', err); - return ShellScriptEditState.NotEdited; - } - } - clearCache(): Promise { - return Promise.resolve(); - } -} - export class GitBashStartupProvider implements ShellStartupScriptProvider { public readonly name: string = 'Git bash'; public readonly shellType: string = ShellConstants.GITBASH; diff --git a/src/features/terminal/shells/providers.ts b/src/features/terminal/shells/providers.ts index 03f01289..6117c01a 100644 --- a/src/features/terminal/shells/providers.ts +++ b/src/features/terminal/shells/providers.ts @@ -1,13 +1,15 @@ import { isWindows } from '../../../common/utils/platformUtils'; import { ShellConstants } from '../../common/shellConstants'; -import { BashEnvsProvider, ZshEnvsProvider } from './bash/bashEnvs'; -import { BashStartupProvider, GitBashStartupProvider, ZshStartupProvider } from './bash/bashStartup'; +import { BashEnvsProvider } from './bash/bashEnvs'; +import { BashStartupProvider, GitBashStartupProvider } from './bash/bashStartup'; import { CmdEnvsProvider } from './cmd/cmdEnvs'; import { CmdStartupProvider } from './cmd/cmdStartup'; import { FishEnvsProvider } from './fish/fishEnvs'; import { FishStartupProvider } from './fish/fishStartup'; import { PowerShellEnvsProvider } from './pwsh/pwshEnvs'; import { PwshStartupProvider } from './pwsh/pwshStartup'; +import { ZshEnvsProvider } from './zsh/zshEnvs'; +import { ZshStartupProvider } from './zsh/zshStartup'; import { ShellEnvsProvider, ShellStartupScriptProvider } from './startupProvider'; export function createShellStartupProviders(): ShellStartupScriptProvider[] { diff --git a/src/features/terminal/shells/zsh/zshConstants.ts b/src/features/terminal/shells/zsh/zshConstants.ts new file mode 100644 index 00000000..1daf23ea --- /dev/null +++ b/src/features/terminal/shells/zsh/zshConstants.ts @@ -0,0 +1,3 @@ +export const ZSH_ENV_KEY = 'VSCODE_PYTHON_ZSH_ACTIVATE'; +export const ZSH_OLD_ENV_KEY = 'VSCODE_ZSH_ACTIVATE'; +export const ZSH_SCRIPT_VERSION = '0.1.1'; diff --git a/src/features/terminal/shells/zsh/zshEnvs.ts b/src/features/terminal/shells/zsh/zshEnvs.ts new file mode 100644 index 00000000..a77761c1 --- /dev/null +++ b/src/features/terminal/shells/zsh/zshEnvs.ts @@ -0,0 +1,52 @@ +import { EnvironmentVariableCollection } from 'vscode'; +import { PythonEnvironment } from '../../../../api'; +import { traceError } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; +import { ShellEnvsProvider } from '../startupProvider'; +import { ZSH_ENV_KEY } from './zshConstants'; + +export class ZshEnvsProvider implements ShellEnvsProvider { + readonly shellType: string = ShellConstants.ZSH; + + updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { + try { + const zshActivation = getShellActivationCommand(this.shellType, env); + if (zshActivation) { + const command = getShellCommandAsString(this.shellType, zshActivation); + const v = collection.get(ZSH_ENV_KEY); + if (v?.value === command) { + return; + } + collection.replace(ZSH_ENV_KEY, command); + } else { + collection.delete(ZSH_ENV_KEY); + } + } catch (err) { + traceError('Failed to update env variables for zsh', err); + collection.delete(ZSH_ENV_KEY); + } + } + + removeEnvVariables(collection: EnvironmentVariableCollection): void { + collection.delete(ZSH_ENV_KEY); + } + + getEnvVariables(env?: PythonEnvironment): Map | undefined { + if (!env) { + return new Map([[ZSH_ENV_KEY, undefined]]); + } + + try { + const zshActivation = getShellActivationCommand(this.shellType, env); + if (zshActivation) { + const command = getShellCommandAsString(this.shellType, zshActivation); + return new Map([[ZSH_ENV_KEY, command]]); + } + return undefined; + } catch (err) { + traceError('Failed to get env variables for zsh', err); + return undefined; + } + } +} diff --git a/src/features/terminal/shells/zsh/zshStartup.ts b/src/features/terminal/shells/zsh/zshStartup.ts new file mode 100644 index 00000000..17be6e82 --- /dev/null +++ b/src/features/terminal/shells/zsh/zshStartup.ts @@ -0,0 +1,155 @@ +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import which from 'which'; +import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; +import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; +import { ZSH_ENV_KEY, ZSH_OLD_ENV_KEY, ZSH_SCRIPT_VERSION } from './zshConstants'; + +async function isZshInstalled(): Promise { + const result = await which('zsh', { nothrow: true }); + return result !== null; +} + +async function getZshProfiles(): Promise { + const zdotdir = process.env.ZDOTDIR; + const baseDir = zdotdir || os.homedir(); + const profile: string = path.join(baseDir, '.zshrc'); + + return profile; +} + +const regionStart = '# >>> vscode python'; +const regionEnd = '# <<< vscode python'; + +function getActivationContent(key: string): string { + const lineSep = '\n'; + return [ + `# version: ${ZSH_SCRIPT_VERSION}`, + `if [ -z "$VSCODE_PYTHON_AUTOACTIVATE_GUARD" ]; then`, + ` export VSCODE_PYTHON_AUTOACTIVATE_GUARD=1`, + ` if [ -n "$${key}" ] && [ "$TERM_PROGRAM" = "vscode" ]; then`, + ` eval "$${key}" || true`, + ` fi`, + `fi`, + ].join(lineSep); +} + +async function isStartupSetup(profile: string, key: string): Promise { + if (await fs.pathExists(profile)) { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + return ShellSetupState.Setup; + } + } + return ShellSetupState.NotSetup; +} + +async function setupStartup(profile: string, key: string, name: string): Promise { + const activationContent = getActivationContent(key); + try { + if (await fs.pathExists(profile)) { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + traceInfo(`SHELL: ${name} profile already contains activation code at: ${profile}`); + } else { + await fs.writeFile(profile, insertStartupCode(content, regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Updated existing ${name} profile at: ${profile}\n${activationContent}`); + } + } else { + await fs.mkdirp(path.dirname(profile)); + await fs.writeFile(profile, insertStartupCode('', regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Created new ${name} profile at: ${profile}\n${activationContent}`); + } + + return true; + } catch (err) { + traceError(`SHELL: Failed to setup startup for profile at: ${profile}`, err); + return false; + } +} + +async function removeStartup(profile: string, key: string): Promise { + if (!(await fs.pathExists(profile))) { + return true; + } + + try { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + await fs.writeFile(profile, removeStartupCode(content, regionStart, regionEnd)); + traceInfo(`SHELL: Removed activation from profile at: ${profile}, for key: ${key}`); + } else { + traceVerbose(`Profile at ${profile} does not contain activation code, for key: ${key}`); + } + return true; + } catch (err) { + traceVerbose(`Failed to remove ${profile} startup, for key: ${key}`, err); + return false; + } +} + +export class ZshStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'zsh'; + public readonly shellType: string = ShellConstants.ZSH; + + private async checkShellInstalled(): Promise { + const found = await isZshInstalled(); + if (!found) { + traceInfo('`zsh` was not found on the system', 'If it is installed make sure it is available on `PATH`'); + } + return found; + } + + async isSetup(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellSetupState.NotInstalled; + } + + try { + const zshProfiles = await getZshProfiles(); + return await isStartupSetup(zshProfiles, ZSH_ENV_KEY); + } catch (err) { + traceError('Failed to check zsh startup scripts', err); + return ShellSetupState.NotSetup; + } + } + + async setupScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + try { + const zshProfiles = await getZshProfiles(); + const result = await setupStartup(zshProfiles, ZSH_ENV_KEY, this.name); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to setup zsh startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + + async teardownScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + try { + const zshProfiles = await getZshProfiles(); + await removeStartup(zshProfiles, ZSH_OLD_ENV_KEY); + const result = await removeStartup(zshProfiles, ZSH_ENV_KEY); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to teardown zsh startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + + clearCache(): Promise { + return Promise.resolve(); + } +}