Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use terminal shell env when resolving commands in path #237588

Draft
wants to merge 54 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
714bb39
start terminal shell env proposed
anthonykim1 Sep 17, 2024
3c412ff
fix typo
anthonykim1 Sep 17, 2024
f214756
progress on shellEnvDetectionCapability, mainThreadTerminalSI
anthonykim1 Sep 19, 2024
3923373
update IShellEnvDetectionCapability interface
anthonykim1 Sep 19, 2024
0465588
touch up on $shellEnvChange
anthonykim1 Sep 19, 2024
e1d611a
adjust IShellEnvDetectionCapability
anthonykim1 Sep 19, 2024
98131e7
properly listen to envChangeEvent
anthonykim1 Sep 19, 2024
e37f119
Serialize env map, expose on exthost
anthonykim1 Nov 4, 2024
a0602bc
Merge remote-tracking branch 'upstream/main' into shellEnvAPI
anthonykim1 Nov 4, 2024
94c6574
start adding to zsh script
anthonykim1 Nov 4, 2024
e4ab9e8
receive environment variable in extension host, properly escape "
anthonykim1 Nov 19, 2024
e2b4ca1
clean up
anthonykim1 Nov 19, 2024
1cf51a5
Add TODO: properly escape double quotes, figure out why JSON parse fa…
anthonykim1 Nov 21, 2024
f0b1578
Fix nonce check, ignore PS1 for now in bash
Tyriar Dec 2, 2024
1b2eb1f
Add some simple PS1 string tests to deserializeMessage
Tyriar Dec 2, 2024
0956b86
New approach of sending env entries separately
Tyriar Dec 2, 2024
bb735f8
be able to get EnvSingleVar
anthonykim1 Dec 3, 2024
a1503da
few comments
anthonykim1 Jan 4, 2025
61653d5
add function signature for start, set, end environment var
anthonykim1 Jan 5, 2025
db981ae
implement EnvStart, EnvEntry, EnvEnd for single env entry
anthonykim1 Jan 6, 2025
a25a9a9
deserialize env value for EnvEntry
anthonykim1 Jan 6, 2025
7fe7c26
Remove unncessary comments
anthonykim1 Jan 6, 2025
338514c
only leave pwsh in this PR and exclude other shells
anthonykim1 Jan 7, 2025
9b97e1b
keep exlcuding other shell env - only pwsh should remain
anthonykim1 Jan 7, 2025
fc9f3f5
Update src/vs/workbench/api/common/extHostTerminalShellIntegration.ts
anthonykim1 Jan 8, 2025
bb93fdd
Update src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts
anthonykim1 Jan 8, 2025
0ca8a75
Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegrat…
anthonykim1 Jan 8, 2025
935f14a
Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegrat…
anthonykim1 Jan 8, 2025
2f7b969
Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegrat…
anthonykim1 Jan 8, 2025
cb997d5
Update src/vs/workbench/api/common/extHostTerminalShellIntegration.ts
anthonykim1 Jan 8, 2025
b6b673f
Update src/vs/platform/terminal/common/capabilities/shellEnvDetection…
anthonykim1 Jan 8, 2025
0ab7322
Update src/vs/workbench/api/common/extHost.protocol.ts
anthonykim1 Jan 8, 2025
fe93145
Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegratio…
anthonykim1 Jan 8, 2025
badfb37
Update src/vs/platform/terminal/common/capabilities/shellEnvDetection…
anthonykim1 Jan 8, 2025
3f156f9
Update src/vs/platform/terminal/common/capabilities/shellEnvDetection…
anthonykim1 Jan 8, 2025
4e8c2cb
Update src/vs/platform/terminal/common/capabilities/shellEnvDetection…
anthonykim1 Jan 8, 2025
333c8d5
Update src/vs/platform/terminal/common/capabilities/capabilities.ts
anthonykim1 Jan 8, 2025
c68c58a
Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegratio…
anthonykim1 Jan 8, 2025
944ce9a
Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
anthonykim1 Jan 8, 2025
b2afe32
Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
anthonykim1 Jan 8, 2025
dbbe2df
Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
anthonykim1 Jan 8, 2025
0b248e2
add comment for ShellEnvDetection
anthonykim1 Jan 8, 2025
d246ba7
change envs in shellEnvDetectionCapability to env
anthonykim1 Jan 8, 2025
6b081da
Mention escaping character for EnvJSON similar to commandLine
anthonykim1 Jan 8, 2025
d555813
Do not fire env event if env has not changed
anthonykim1 Jan 8, 2025
3591fa4
add link to CommandLine
anthonykim1 Jan 9, 2025
143a4cc
follow main branch format so I avoid merge conflict
anthonykim1 Jan 9, 2025
6509fa1
Merge branch 'main' into shellEnvPwshOnly
anthonykim1 Jan 9, 2025
03fe326
remove resolved TODO
anthonykim1 Jan 9, 2025
d501cee
Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegratio…
anthonykim1 Jan 9, 2025
e94cf4d
Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
anthonykim1 Jan 9, 2025
0b831a5
use vscode object equals
anthonykim1 Jan 9, 2025
5f9f2db
Merge branch 'main' into shellEnvPwshOnly
Tyriar Jan 9, 2025
4cbdc0a
Use terminal shell env when resolving commands in path
Tyriar Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/terminal-suggest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"Other"
],
"enabledApiProposals": [
"terminalCompletionProvider"
"terminalCompletionProvider",
"terminalShellEnv"
],
"scripts": {
"compile": "npx gulp compile-extension:terminal-suggest",
Expand Down
31 changes: 24 additions & 7 deletions extensions/terminal-suggest/src/terminalSuggestMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import codeInsidersCompletionSpec from './completions/code-insiders';
import codeCompletionSpec from './completions/code';
import cdSpec from './completions/cd';

let cachedAvailableCommandsPath: string | undefined;
let cachedAvailableCommands: Set<string> | undefined;
const cachedBuiltinCommands: Map<string, string[] | undefined> = new Map();

Expand Down Expand Up @@ -80,7 +81,7 @@ export async function activate(context: vscode.ExtensionContext) {
return;
}

const commandsInPath = await getCommandsInPath();
const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env);
const builtinCommands = getBuiltinCommands(shellPath);
if (!commandsInPath || !builtinCommands) {
return;
Expand Down Expand Up @@ -129,6 +130,7 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U

// Resolve the absolute path of the prefix
const resolvedPath = path.resolve(currentCwd?.fsPath, relativeFolder);

const stat = await fs.stat(resolvedPath);

// Check if the resolved path exists and is a directory
Expand Down Expand Up @@ -187,15 +189,30 @@ async function isExecutable(filePath: string): Promise<boolean> {
}
}

async function getCommandsInPath(): Promise<Set<string> | undefined> {
if (cachedAvailableCommands) {
return cachedAvailableCommands;
async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<Set<string> | undefined> {
// Get PATH value
let pathValue: string | undefined;
if (osIsWindows()) {
const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path');
if (caseSensitivePathKey) {
pathValue = env[caseSensitivePathKey];
}
} else {
pathValue = env.PATH;
}
const paths = osIsWindows() ? process.env.PATH?.split(';') : process.env.PATH?.split(':');
if (!paths) {
if (pathValue === undefined) {
return;
}
const pathSeparator = osIsWindows() ? '\\' : '/';

// Check cache
if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) {
return cachedAvailableCommands;
}

// Extract executables from PATH
const isWindows = osIsWindows();
const paths = pathValue.split(isWindows ? ';' : ':');
const pathSeparator = isWindows ? '\\' : '/';
const executables = new Set<string>();
for (const path of paths) {
try {
Expand Down
3 changes: 2 additions & 1 deletion extensions/terminal-suggest/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"src/**/*",
"src/completions/index.d.ts",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts"
"../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts"
]
}
3 changes: 3 additions & 0 deletions src/vs/platform/extensions/common/extensionsApiProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ const _allApiProposals = {
terminalSelection: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts',
},
terminalShellEnv: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts',
},
testObserver: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts',
},
Expand Down
16 changes: 15 additions & 1 deletion src/vs/platform/terminal/common/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ export const enum TerminalCapability {
* the request (task, debug, etc) provides an ID, optional marker, hoverMessage, and hidden property. When
* hidden is not provided, a generic decoration is added to the buffer and overview ruler.
*/
BufferMarkDetection
BufferMarkDetection,

/**
* The terminal can detect the latest environment of user's current shell.
*/
ShellEnvDetection,

}

/**
Expand Down Expand Up @@ -133,6 +139,7 @@ export interface ITerminalCapabilityImplMap {
[TerminalCapability.NaiveCwdDetection]: INaiveCwdDetectionCapability;
[TerminalCapability.PartialCommandDetection]: IPartialCommandDetectionCapability;
[TerminalCapability.BufferMarkDetection]: IBufferMarkCapability;
[TerminalCapability.ShellEnvDetection]: IShellEnvDetectionCapability;
}

export interface ICwdDetectionCapability {
Expand All @@ -143,6 +150,13 @@ export interface ICwdDetectionCapability {
updateCwd(cwd: string): void;
}

export interface IShellEnvDetectionCapability {
readonly type: TerminalCapability.ShellEnvDetection;
readonly onDidChangeEnv: Event<Map<string, string>>;
get env(): Map<string, string>;
setEnvironment(envs: { [key: string]: string | undefined } | undefined, isTrusted: boolean): void;
}

export const enum CommandInvalidationReason {
Windows = 'windows',
NoProblemsReported = 'noProblemsReported'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable } from '../../../../base/common/lifecycle.js';
import { IShellEnvDetectionCapability, TerminalCapability } from './capabilities.js';
import { Emitter } from '../../../../base/common/event.js';
import { equals } from '../../../../base/common/objects.js';

export class ShellEnvDetectionCapability extends Disposable implements IShellEnvDetectionCapability {
readonly type = TerminalCapability.ShellEnvDetection;

private readonly _env: Map<string, string> = new Map();
get env(): Map<string, string> { return this._env; }

private readonly _onDidChangeEnv = this._register(new Emitter<Map<string, string>>());
readonly onDidChangeEnv = this._onDidChangeEnv.event;

setEnvironment(env: { [key: string]: string | undefined }, isTrusted: boolean): void {
if (!isTrusted) {
return;
}

if (equals(this._env, env)) {
return;
}

this._env.clear();
for (const [key, value] of Object.entries(env)) {
if (value !== undefined) {
this._env.set(key, value);
}
}

// Convert to event and fire event
this._onDidChangeEnv.fire(this._env);
}
}
39 changes: 38 additions & 1 deletion src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base
import { TerminalCapabilityStore } from '../capabilities/terminalCapabilityStore.js';
import { CommandDetectionCapability } from '../capabilities/commandDetectionCapability.js';
import { CwdDetectionCapability } from '../capabilities/cwdDetectionCapability.js';
import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, TerminalCapability } from '../capabilities/capabilities.js';
import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, IShellEnvDetectionCapability, TerminalCapability } from '../capabilities/capabilities.js';
import { PartialCommandDetectionCapability } from '../capabilities/partialCommandDetectionCapability.js';
import { ILogService } from '../../../log/common/log.js';
import { ITelemetryService } from '../../../telemetry/common/telemetry.js';
Expand All @@ -18,6 +18,7 @@ import type { ITerminalAddon, Terminal } from '@xterm/headless';
import { URI } from '../../../../base/common/uri.js';
import { sanitizeCwd } from '../terminalEnvironment.js';
import { removeAnsiEscapeCodesFromPrompt } from '../../../../base/common/strings.js';
import { ShellEnvDetectionCapability } from '../capabilities/shellEnvDetectionCapability.js';


/**
Expand Down Expand Up @@ -224,6 +225,20 @@ const enum VSCodeOscPt {
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
SetMark = 'SetMark',

/**
* Sends the shell's complete environment in JSON format.
*
* Format: `OSC 633 ; EnvJson ; <Environment> ; <Nonce>`
*
* - `Environment` - A stringified JSON object containing the shell's complete environment. The
* variables and values use the same encoding rules as the {@link CommandLine} sequence.
* - `Nonce` - An _mandatory_ nonce to ensure the sequence is not malicious.
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
EnvJson = 'EnvJson',

}

/**
Expand Down Expand Up @@ -418,6 +433,19 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
this._createOrGetCommandDetection(this._terminal).handleContinuationEnd();
return true;
}
case VSCodeOscPt.EnvJson: {
const arg0 = args[0];
const arg1 = args[1];
if (arg0 !== undefined) {
try {
const env = JSON.parse(deserializeMessage(arg0));
this._createOrGetShellEnvDetection().setEnvironment(env, arg1 === this._nonce);
} catch (e) {
this._logService.warn('Failed to parse environment from shell integration sequence', arg0);
}
}
return true;
}
case VSCodeOscPt.RightPromptStart: {
this._createOrGetCommandDetection(this._terminal).handleRightPromptStart();
return true;
Expand Down Expand Up @@ -614,6 +642,15 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
}
return bufferMarkDetection;
}

protected _createOrGetShellEnvDetection(): IShellEnvDetectionCapability {
let shellEnvDetection = this.capabilities.get(TerminalCapability.ShellEnvDetection);
if (!shellEnvDetection) {
shellEnvDetection = this._register(new ShellEnvDetectionCapability());
this.capabilities.add(TerminalCapability.ShellEnvDetection, shellEnvDetection);
}
return shellEnvDetection;
}
}

export function deserializeMessage(message: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma
this._proxy.$cwdChange(e.instance.instanceId, this._convertCwdToUri(e.data));
}));

// onDidChangeTerminalShellIntegration via env
const envChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.ShellEnvDetection, e => e.onDidChangeEnv));
this._store.add(envChangeEvent.event(e => {
const keysArr = Array.from(e.data.keys());
const valuesArr = Array.from(e.data.values());
this._proxy.$shellEnvChange(e.instance.instanceId, keysArr, valuesArr);
}));

// onDidStartTerminalShellExecution
const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted));
let currentCommand: ITerminalCommand | undefined;
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,7 @@ export interface ExtHostTerminalShellIntegrationShape {
$shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void;
$shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void;
$shellExecutionData(instanceId: number, data: string): void;
$shellEnvChange(instanceId: number, shellEnvKeys: string[], shellEnvValues: string[]): void;
$cwdChange(instanceId: number, cwd: UriComponents | undefined): void;
$closeTerminal(instanceId: number): void;
}
Expand Down
23 changes: 22 additions & 1 deletion src/vs/workbench/api/common/extHostTerminalShellIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH
this._activeShellIntegrations.get(instanceId)?.emitData(data);
}

public $shellEnvChange(instanceId: number, shellEnvKeys: string[], shellEnvValues: string[]): void {
this._activeShellIntegrations.get(instanceId)?.setEnv(shellEnvKeys, shellEnvValues);
}

public $cwdChange(instanceId: number, cwd: UriComponents | undefined): void {
this._activeShellIntegrations.get(instanceId)?.setCwd(URI.revive(cwd));
}
Expand All @@ -147,6 +151,7 @@ class InternalTerminalShellIntegration extends Disposable {
get currentExecution(): InternalTerminalShellExecution | undefined { return this._currentExecution; }

private _ignoreNextExecution: boolean = false;
private _env: { [key: string]: string | undefined } | undefined;
private _cwd: URI | undefined;

readonly store: DisposableStore = this._register(new DisposableStore());
Expand All @@ -171,6 +176,9 @@ class InternalTerminalShellIntegration extends Disposable {
get cwd(): URI | undefined {
return that._cwd;
},
get env(): { [key: string]: string | undefined } | undefined {
return that._env;
},
// executeCommand(commandLine: string): vscode.TerminalShellExecution;
// executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution;
executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution {
Expand Down Expand Up @@ -232,6 +240,15 @@ class InternalTerminalShellIntegration extends Disposable {
}
}

setEnv(keys: string[], values: string[]): void {
const env: { [key: string]: string | undefined } = {};
for (let i = 0; i < keys.length; i++) {
env[keys[i]] = values[i];
}
this._env = env;
this._fireChangeEvent();
}

setCwd(cwd: URI | undefined): void {
let wasChanged = false;
if (URI.isUri(this._cwd)) {
Expand All @@ -241,9 +258,13 @@ class InternalTerminalShellIntegration extends Disposable {
}
if (wasChanged) {
this._cwd = cwd;
this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value });
this._fireChangeEvent();
}
}

private _fireChangeEvent() {
this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value });
}
}

class InternalTerminalShellExecution {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ function Global:Prompt() {
# Current working directory
# OSC 633 ; <Property>=<Value> ST
$Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a" }

# Send current environment variables as JSON
# OSC 633 ; Env ; <Environment> ; <Nonce>
if ($isStable -eq "0") {
$envMap = @{}
Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value }
$envJson = $envMap | ConvertTo-Json -Compress
$Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a"
}

# Before running the original prompt, put $? back to what it was:
if ($FakeCode -ne 0) {
Write-Error "failure" -ea ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ suite('ShellIntegrationAddon', () => {
['escaped newline (upper hex)', `${Backslash}x0A`, Newline],
['escaped backslash followed by literal "x0a" is not a newline', `${Backslash}${Backslash}x0a`, `${Backslash}x0a`],
['non-initial escaped backslash followed by literal "x0a" is not a newline', `foo${Backslash}${Backslash}x0a`, `foo${Backslash}x0a`],
['PS1 simple', '[\\u@\\h \\W]\\$', '[\\u@\\h \\W]\\$'],
['PS1 VSC SI', `${Backslash}x1b]633;A${Backslash}x07\\[${Backslash}x1b]0;\\u@\\h:\\w\\a\\]${Backslash}x1b]633;B${Backslash}x07`, '\x1b]633;A\x07\\[\x1b]0;\\u@\\h:\\w\\a\\]\x1b]633;B\x07']
];

cases.forEach(([title, input, expected]) => {
Expand Down
18 changes: 18 additions & 0 deletions src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

declare module 'vscode' {
// @anthonykim1 @tyriar https://github.com/microsoft/vscode/issues/227467

export interface TerminalShellIntegration {
/**
* The environment of the shell process. This is undefined if the shell integration script
* does not send the environment.
*/
readonly env: { [key: string]: string | undefined } | undefined;
}

// TODO: Is it fine that this shares onDidChangeTerminalShellIntegration with cwd and the shellIntegration object itself?
}
Loading