src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts
443 LOC · 327 covered · 116 uncovered · 90 ranges · 52 concepts · 42 introducers · 44 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
terminalEnvironment.ts ×15
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* This module contains utility functions related to the environment, cwd and paths.
*/
import * as path from '../../../../base/common/path.js';
import { URI, uriToFsPath } from '../../../../base/common/uri.js';
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js';
import { sanitizeProcessEnvironment } from '../../../../base/common/processes.js';
import { IShellLaunchConfig, ITerminalBackend, ITerminalEnvironment, ShellIntegrationTimeoutOverride, TerminalSettingId, TerminalShellType, WindowsShellType } from '../../../../platform/terminal/common/terminal.js';
import { IProcessEnvironment, isWindows, isMacintosh, language, OperatingSystem } from '../../../../base/common/platform.js';
import { escapeNonWindowsPath, sanitizeCwd } from '../../../../platform/terminal/common/terminalEnvironment.js';
import { isNumber, isString } from '../../../../base/common/types.js';
import { IHistoryService } from '../../../services/history/common/history.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import type { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
export function mergeEnvironments(parent: IProcessEnvironment, other: ITerminalEnvironment | undefined): void {
}
// On Windows apply the new values ignoring case, while still retaining
// the case of the original key.
if (isWindows) {
for (const configKey in other) {
let actualKey = configKey;
for (const envKey in parent) {
if (configKey.toLowerCase() === envKey.toLowerCase()) {
actualKey = envKey;
break;
}
}
const value = other[configKey];
if (value !== undefined) {
_mergeEnvironmentValue(parent, actualKey, value);
}
}
Object.keys(other).forEach((key) => {
if (value !== undefined) {
_mergeEnvironmentValue(parent, key, value);
}
}
}
function _mergeEnvironmentValue(env: ITerminalEnvironment, key: string, value: string | null): void {
terminalEnvironment.ts ×4
if (isString(value)) {
}
export function addTerminalEnvironmentKeys(env: IProcessEnvironment, version: string | undefined, locale: string | undefined, detectLocale: 'auto' | 'off' | 'on'): void {
if (version) {
}
}
}
function mergeNonNullKeys(env: IProcessEnvironment, other: ITerminalEnvironment | undefined) {
terminalEnvironment.ts ×7
if (!other) {
return;
}
const value = other[key];
if (value !== undefined && value !== null) {
env[key] = value;
}
}
}
async function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): Promise<ITerminalEnvironment> {
await Promise.all(Object.entries(env).map(async ([key, value]) => {
if (isString(value)) {
try {
env[key] = await variableResolver(value);
} catch (e) {
env[key] = value;
}
}
}));
return env;
}
export function shouldSetLangEnvVariable(env: IProcessEnvironment, detectLocale: 'auto' | 'off' | 'on'): boolean {
}
return !lang || (lang.search(/\.UTF\-8$/) === -1 && lang.search(/\.utf8$/) === -1 && lang.search(/\.euc.+/) === -1);
}
}
export function getLangEnvVariable(locale?: string): string {
const n = parts.length;
if (n === 0) {
return 'en_US.UTF-8';
}
// The local may only contain the language, not the variant, if this is the case guess the
terminalEnvironment.ts ×1
// variant such that it can be used as a valid $LANG variable. The language variant chosen
// is the original and/or most prominent with help from
// https://stackoverflow.com/a/2502675/1156119
// The list of locales was generated by running `locale -a` on macOS
const languageVariants: { [key: string]: string } = {
af: 'ZA',
am: 'ET',
be: 'BY',
bg: 'BG',
ca: 'ES',
cs: 'CZ',
da: 'DK',
// de: 'AT',
// de: 'CH',
de: 'DE',
el: 'GR',
// en: 'AU',
// en: 'CA',
// en: 'GB',
// en: 'IE',
// en: 'NZ',
en: 'US',
es: 'ES',
et: 'EE',
eu: 'ES',
fi: 'FI',
// fr: 'BE',
// fr: 'CA',
// fr: 'CH',
fr: 'FR',
he: 'IL',
hr: 'HR',
hu: 'HU',
hy: 'AM',
is: 'IS',
// it: 'CH',
it: 'IT',
ja: 'JP',
kk: 'KZ',
ko: 'KR',
lt: 'LT',
// nl: 'BE',
nl: 'NL',
no: 'NO',
pl: 'PL',
pt: 'BR',
// pt: 'PT',
ro: 'RO',
ru: 'RU',
sk: 'SK',
sl: 'SI',
sr: 'YU',
sv: 'SE',
tr: 'TR',
uk: 'UA',
zh: 'CN',
};
if (Object.prototype.hasOwnProperty.call(languageVariants, parts[0])) {
parts.push(languageVariants[parts[0]]);
}
parts[1] = parts[1].toUpperCase();
}
}
shell: IShellLaunchConfig,
userHome: string | undefined,
variableResolver: VariableResolver | undefined,
root: URI | undefined,
customCwd: string | undefined,
logService?: ILogService
): Promise<string> {
if (shell.cwd) {
const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd;
const resolved = await _resolveCwd(unresolved, variableResolver);
return sanitizeCwd(resolved || unresolved);
}
let cwd: string | undefined;
if (!shell.ignoreConfigurationCwd && customCwd) {
customCwd = await _resolveCwd(customCwd, variableResolver, logService);
}
if (path.isAbsolute(customCwd)) {
}
}
// If there was no custom cwd or it was relative with no workspace
if (!cwd) {
}
return sanitizeCwd(cwd);
}
async function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): Promise<string | undefined> {
if (variableResolver) {
try {
return await variableResolver(cwd);
} catch (e) {
logService?.error('Could not resolve terminal cwd', e);
return undefined;
}
}
return cwd;
}
export type VariableResolver = (str: string) => Promise<string>;
export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | undefined, env: IProcessEnvironment, configurationResolverService: IConfigurationResolverService | undefined): VariableResolver | undefined {
if (!configurationResolverService) {
return undefined;
}
return (str) => configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, str);
}
shellLaunchConfig: IShellLaunchConfig,
envFromConfig: ITerminalEnvironment | undefined,
variableResolver: VariableResolver | undefined,
version: string | undefined,
detectLocale: 'auto' | 'off' | 'on',
baseEnv: IProcessEnvironment
): Promise<IProcessEnvironment> {
// Create a terminal environment based on settings, launch config and permissions
const env: IProcessEnvironment = {};
if (shellLaunchConfig.strictEnv) {
// strictEnv is true, only use the requested env (ignoring null entries)
mergeNonNullKeys(env, shellLaunchConfig.env);
// Merge process env with the env from config and from shellLaunchConfig
mergeNonNullKeys(env, baseEnv);
const allowedEnvFromConfig = { ...envFromConfig };
// Resolve env vars from config and shell
if (variableResolver) {
if (allowedEnvFromConfig) {
await resolveConfigurationVariables(variableResolver, allowedEnvFromConfig);
}
if (shellLaunchConfig.env) {
await resolveConfigurationVariables(variableResolver, shellLaunchConfig.env);
}
}
// Workaround for https://github.com/microsoft/vscode/issues/204005
// We should restore the following environment variables when a user
// launches the application using the CLI so that integrated terminal
// can still inherit these variables.
// We are not bypassing the restrictions implied in https://github.com/electron/electron/pull/40770
// since this only affects integrated terminal and not the application itself.
if (isMacintosh) {
// Restore NODE_OPTIONS if it was set
if (env['VSCODE_NODE_OPTIONS']) {
env['NODE_OPTIONS'] = env['VSCODE_NODE_OPTIONS'];
delete env['VSCODE_NODE_OPTIONS'];
}
// Restore NODE_REPL_EXTERNAL_MODULE if it was set
if (env['VSCODE_NODE_REPL_EXTERNAL_MODULE']) {
env['NODE_REPL_EXTERNAL_MODULE'] = env['VSCODE_NODE_REPL_EXTERNAL_MODULE'];
delete env['VSCODE_NODE_REPL_EXTERNAL_MODULE'];
}
}
// Sanitize the environment, removing any undesirable VS Code and Electron environment
// variables
sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI');
// Merge config (settings) and ShellLaunchConfig environments
mergeEnvironments(env, allowedEnvFromConfig);
mergeEnvironments(env, shellLaunchConfig.env);
// Adding other env keys necessary to create the process
addTerminalEnvironmentKeys(env, version, language, detectLocale);
}
return env;
}
/**
* Takes a path and returns the properly escaped path to send to a given shell. On Windows, this
* included trying to prepare the path for WSL if needed.
*
* @param originalPath The path to be escaped and formatted.
* @param executable The executable off the shellLaunchConfig.
* @param title The terminal's title.
* @param shellType The type of shell the path is being sent to.
* @param backend The backend for the terminal.
* @param isWindowsFrontend Whether the frontend is Windows, this is only exposed for injection via
* tests.
* @returns An escaped version of the path to be executed in the terminal.
*/
export async function preparePathForShell(resource: string | URI, executable: string | undefined, title: string, shellType: TerminalShellType | undefined, backend: Pick<ITerminalBackend, 'getWslPath'> | undefined, os: OperatingSystem | undefined, isWindowsFrontend: boolean = isWindows): Promise<string> {
terminalEnvironment.ts ×6
let originalPath: string;
if (isString(resource)) {
originalPath = resource;
} else {
originalPath = resource.fsPath;
// Apply backend OS-specific formatting to the path since URI.fsPath uses the frontend's OS
if (isWindowsFrontend && os !== OperatingSystem.Windows) {
originalPath = originalPath.replace(/\\/g, '\/');
} else if (!isWindowsFrontend && os === OperatingSystem.Windows) {
originalPath = originalPath.replace(/\//g, '\\');
}
}
if (!executable) {
return originalPath;
}
const hasSpace = originalPath.includes(' ');
const hasParens = originalPath.includes('(') || originalPath.includes(')');
const pathBasename = path.basename(executable, '.exe');
const isPowerShell = pathBasename === 'pwsh' ||
pathBasename === 'powershell' ||
title === 'powershell';
if (isPowerShell && (hasSpace || originalPath.includes('\''))) {
}
if (hasParens && isPowerShell) {
return `& '${originalPath}'`;
}
if (os === OperatingSystem.Windows) {
// Update Windows uriPath to be executed in WSL.
if (shellType !== undefined) {
if (shellType === WindowsShellType.GitBash) {
return escapeNonWindowsPath(originalPath.replace(/\\/g, '/'), shellType);
terminalEnvironment.ts ×1
}
return backend?.getWslPath(originalPath, 'win-to-unix') || originalPath;
terminalEnvironment.ts ×1
}
}
}
const lowerExecutable = executable.toLowerCase();
if (lowerExecutable.includes('wsl') || (lowerExecutable.includes('bash.exe') && !lowerExecutable.toLowerCase().includes('git'))) {
terminalEnvironment.ts ×2
return backend?.getWslPath(originalPath, 'win-to-unix') || originalPath;
} else if (hasSpace) {
return `"${originalPath}"`;
}
return originalPath;
}
return escapeNonWindowsPath(originalPath, shellType);
}
export function getWorkspaceForTerminal(cwd: URI | string | undefined, workspaceContextService: IWorkspaceContextService, historyService: IHistoryService): IWorkspaceFolder | undefined {
let workspaceFolder = cwdUri ? workspaceContextService.getWorkspaceFolder(cwdUri) ?? undefined : undefined;
if (!workspaceFolder) {
// fallback to last active workspace if cwd is not available or it is not in workspace
terminalEnvironment.ts ×1
// TOOD: last active workspace is known to be unreliable, we should remove this fallback eventually
const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot();
workspaceFolder = activeWorkspaceRootUri ? workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) ?? undefined : undefined;
}
}
export async function getUriLabelForShell(uri: URI | string, backend: Pick<ITerminalBackend, 'getWslPath'>, shellType?: TerminalShellType, os?: OperatingSystem, isWindowsFrontend: boolean = isWindows): Promise<string> {
terminalEnvironment.ts ×3
let path = isString(uri) ? uri : uri.fsPath;
if (os === OperatingSystem.Windows) {
return backend.getWslPath(path.replaceAll('/', '\\'), 'win-to-unix');
terminalEnvironment.ts ×1
return path.replaceAll('\\', '/').replace(/^([a-zA-Z]):\//, '/$1/');
// If the frontend is not Windows but the terminal is, convert / to \.
terminalEnvironment.ts ×1
path = isString(uri) ? path : uriToFsPath(uri, true);
return !isWindowsFrontend ? path.replaceAll('/', '\\') : path;
}
// If the frontend is Windows but the terminal is not, convert \ to /.
terminalEnvironment.ts ×1
return isWindowsFrontend ? path.replaceAll('\\', '/') : path;
}
/**
* Gets the unified duration to wait for shell integration after the terminal launches before
* declaring the terminal lacks shell integration.
*/
export function getShellIntegrationTimeout(
configurationService: IConfigurationService,
siInjectionEnabled: boolean,
isRemote: boolean,
processReadyTimestamp?: number
): number {
const timeoutValue = configurationService.getValue<unknown>(TerminalSettingId.ShellIntegrationTimeout);
let timeoutMs: number;
if (isNumber(timeoutValue) && timeoutValue === ShellIntegrationTimeoutOverride.DisableForTests) {
// Used for tests
timeoutMs = 0;
} else if (!isNumber(timeoutValue) || timeoutValue < 0) {
timeoutMs = siInjectionEnabled ? 5000 : (isRemote ? 3000 : 2000);
} else if (timeoutValue === 0) {
timeoutMs = 0;
} else {
timeoutMs = Math.max(timeoutValue, 500);
}
// Adjust timeout based on how long the process has already been running
if (processReadyTimestamp !== undefined) {
const elapsed = Date.now() - processReadyTimestamp;
timeoutMs = Math.max(0, timeoutMs - elapsed);
}
return timeoutMs;
}