diff --git a/extensions/configuration-editing/src/configurationEditingMain.ts b/extensions/configuration-editing/src/configurationEditingMain.ts index cc8fc81576f..a90364ad93e 100644 --- a/extensions/configuration-editing/src/configurationEditingMain.ts +++ b/extensions/configuration-editing/src/configurationEditingMain.ts @@ -40,7 +40,7 @@ function registerVariableCompletions(pattern: string): vscode.Disposable { provideCompletionItems(document, position, _token) { const location = getLocation(document.getText(), document.offsetAt(position)); if (!location.isAtPropertyKey && location.previousNode && location.previousNode.type === 'string') { - const indexOf$ = document.lineAt(position.line).text.indexOf('$'); + const indexOf$ = document.lineAt(position.line).text.lastIndexOf('$', position.character); const startPosition = indexOf$ >= 0 ? new vscode.Position(position.line, indexOf$) : position; return [ @@ -58,9 +58,11 @@ function registerVariableCompletions(pattern: string): vscode.Disposable { { label: 'fileBasenameNoExtension', detail: localize('fileBasenameNoExtension', "The current opened file's basename with no file extension") }, { label: 'defaultBuildTask', detail: localize('defaultBuildTask', "The name of the default build task. If there is not a single default build task then a quick pick is shown to choose the build task.") }, { label: 'pathSeparator', detail: localize('pathSeparator', "The character used by the operating system to separate components in file paths") }, + { label: 'extensionInstallFolder', detail: localize('extensionInstallFolder', "The path where an an extension is installed."), param: 'publisher.extension' }, ].map(variable => ({ - label: '${' + variable.label + '}', + label: `\${${variable.label}}`, range: new vscode.Range(startPosition, position), + insertText: variable.param ? new vscode.SnippetString(`\${${variable.label}:`).appendPlaceholder(variable.param).appendText('}') : (`\${${variable.label}}`), detail: variable.detail })); } diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 0037912f68a..8aa9e50c9e9 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -274,6 +274,29 @@ export function lastNonWhitespaceIndex(str: string, startIndex: number = str.len return -1; } +/** + * Function that works identically to String.prototype.replace, except, the + * replace function is allowed to be async and return a Promise. + */ +export function replaceAsync(str: string, search: RegExp, replacer: (match: string, ...args: any[]) => Promise): Promise { + let parts: (string | Promise)[] = []; + + let last = 0; + for (const match of str.matchAll(search)) { + parts.push(str.slice(last, match.index)); + if (match.index === undefined) { + throw new Error('match.index should be defined'); + } + + last = match.index + match[0].length; + parts.push(replacer(match[0], ...match.slice(1), match.index, str, match.groups)); + } + + parts.push(str.slice(last)); + + return Promise.all(parts).then(p => p.join('')); +} + export function compare(a: string, b: string): number { if (a < b) { return -1; diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 8d27e715931..6744ea9a371 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -386,4 +386,13 @@ suite('Strings', () => { assert.strictEqual('hello world', strings.truncate('hello world', 100)); assert.strictEqual('hello…', strings.truncate('hello world', 5)); }); + + test('replaceAsync', async () => { + let i = 0; + assert.strictEqual(await strings.replaceAsync('abcabcabcabc', /b(.)/g, async (match, after) => { + assert.strictEqual(match, 'bc'); + assert.strictEqual(after, 'c'); + return `${i++}${after}`; + }), 'a0ca1ca2ca3c'); + }); }); diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index 792b3a0274f..5c14bf3006c 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -29,13 +29,15 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura import { buildUserEnvironment } from 'vs/server/node/extensionHostConnection'; import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( env: platform.IProcessEnvironment, workspaceFolders: IWorkspaceFolder[], activeFileResource: URI | undefined, - resolvedVariables: { [name: string]: string } + resolvedVariables: { [name: string]: string }, + extensionService: IExtensionManagementService, ) { super({ getFolderUri: (folderName: string): URI | undefined => { @@ -68,7 +70,12 @@ class CustomVariableResolver extends AbstractVariableResolverService { }, getLineNumber: (): string | undefined => { return resolvedVariables['lineNumber']; - } + }, + getExtension: async id => { + const installed = await extensionService.getInstalled(); + const found = installed.find(e => e.identifier.id === id); + return found && { extensionLocation: found.location }; + }, }, undefined, Promise.resolve(os.homedir()), Promise.resolve(env)); } } @@ -89,7 +96,8 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< private readonly _environmentService: IServerEnvironmentService, private readonly _logService: ILogService, private readonly _ptyService: IPtyService, - private readonly _productService: IProductService + private readonly _productService: IProductService, + private readonly _extensionManagementService: IExtensionManagementService, ) { super(); } @@ -196,16 +204,16 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< const workspaceFolders = args.workspaceFolders.map(reviveWorkspaceFolder); const activeWorkspaceFolder = args.activeWorkspaceFolder ? reviveWorkspaceFolder(args.activeWorkspaceFolder) : undefined; const activeFileResource = args.activeFileResource ? URI.revive(uriTransformer.transformIncoming(args.activeFileResource)) : undefined; - const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables); + const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables, this._extensionManagementService); const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, process.env, customVariableResolver); // Get the initial cwd - const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); + const initialCwd = await terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); shellLaunchConfig.cwd = initialCwd; const envPlatformKey = platform.isWindows ? 'terminal.integrated.env.windows' : (platform.isMacintosh ? 'terminal.integrated.env.osx' : 'terminal.integrated.env.linux'); const envFromConfig = args.configuration[envPlatformKey]; - const env = terminalEnvironment.createTerminalEnvironment( + const env = await terminalEnvironment.createTerminalEnvironment( shellLaunchConfig, envFromConfig, variableResolver, @@ -222,7 +230,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< } const envVariableCollections = new Map(entries); const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); - mergedCollection.applyToProcessEnvironment(env); + await mergedCollection.applyToProcessEnvironment(env, variableResolver); } // Fork the process and listen for messages diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 82a43fc6717..8e14d6fdc2b 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -186,7 +186,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), appInsightsAppender); socketServer.registerChannel('telemetry', telemetryChannel); - socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService)); + socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService, extensionManagementService)); const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService); socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 4bc58f95e00..1b9316170ad 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -57,6 +57,9 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha public dispose(): void { } + $getExtension(extensionId: string) { + return this._extensionService.getExtension(extensionId); + } $activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { return this._internalExtensionService._activateById(extensionId, reason); } diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index e8ec83f8d66..9ffb9e748b6 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -26,6 +26,7 @@ import { ExtHostEditorTabs, IExtHostEditorTabs } from 'vs/workbench/api/common/e import { ExtHostLoggerService } from 'vs/workbench/api/common/extHostLoggerService'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; +import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; registerSingleton(ILoggerService, ExtHostLoggerService); registerSingleton(ILogService, ExtHostLogService); @@ -48,3 +49,4 @@ registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostSecretState, ExtHostSecretState); registerSingleton(IExtHostTelemetry, ExtHostTelemetry); registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs); +registerSingleton(IExtHostVariableResolverProvider, ExtHostVariableResolverProviderService); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 307d7340923..0e95b9827b8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1107,6 +1107,7 @@ export interface MainThreadTaskShape extends IDisposable { } export interface MainThreadExtensionServiceShape extends IDisposable { + $getExtension(extensionId: string): Promise | undefined>; $activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; $onWillActivateExtension(extensionId: ExtensionIdentifier): Promise; $onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 1db3ef2485d..178ab59c5ef 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -3,36 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'vs/base/common/path'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; import { asPromise } from 'vs/base/common/async'; -import { - MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, - IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto -} from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, TextDiffTabInput, NotebookDiffEditorTabInput, TextTabInput, NotebookEditorTabInput, CustomEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; -import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; -import { convertToVSCPaths, convertToDAPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -import { ISignService } from 'vs/platform/sign/common/sign'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import type * as vscode from 'vscode'; +import { Emitter, Event } from 'vs/base/common/event'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import * as process from 'vs/base/common/process'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, SourceBreakpoint } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; +import { IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution } from 'vs/workbench/contrib/debug/common/debug'; +import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; +import type * as vscode from 'vscode'; +import { IExtHostConfiguration } from '../common/extHostConfiguration'; +import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -101,17 +94,15 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private _debugAdapters: Map; private _debugAdaptersTrackers: Map; - private _variableResolver: IConfigurationResolverService | undefined; - private _signService: ISignService | undefined; constructor( @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace protected _workspaceService: IExtHostWorkspace, @IExtHostExtensionService private _extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors private _editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration protected _configurationService: IExtHostConfiguration, - @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, ) { this._configProviderHandleCounter = 0; this._configProviders = []; @@ -371,13 +362,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return Promise.resolve(undefined); } - protected abstract createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService; - public async $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): Promise { - if (!this._variableResolver) { - const [workspaceFolders, configProvider] = await Promise.all([this._workspaceService.getWorkspaceFolders2(), this._configurationService.getConfigProvider()]); - this._variableResolver = this.createVariableResolver(workspaceFolders || [], this._editorsService, configProvider!); - } let ws: IWorkspaceFolder | undefined; const folder = await this.getFolder(folderUri); if (folder) { @@ -390,7 +375,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } }; } - return this._variableResolver.resolveAnyAsync(ws, config); + const variableResolver = await this._variableResolver.getResolver(); + return variableResolver.resolveAnyAsync(ws, config); } protected createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -939,94 +925,6 @@ export class ExtHostDebugConsole { } } -export class ExtHostVariableResolverService extends AbstractVariableResolverService { - - constructor(folders: vscode.WorkspaceFolder[], - editorService: ExtHostDocumentsAndEditors | undefined, - configurationService: ExtHostConfigProvider, - editorTabs: IExtHostEditorTabs, - workspaceService?: IExtHostWorkspace, - userHome?: string) { - function getActiveUri(): URI | undefined { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return activeEditor.document.uri; - } - const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab; - if (activeTab !== undefined) { - // Resolve a resource from the tab - if (activeTab.kind instanceof TextDiffTabInput || activeTab.kind instanceof NotebookDiffEditorTabInput) { - return activeTab.kind.modified; - } else if (activeTab.kind instanceof TextTabInput || activeTab.kind instanceof NotebookEditorTabInput || activeTab.kind instanceof CustomEditorTabInput) { - return activeTab.kind.uri; - } - } - } - return undefined; - } - - super({ - getFolderUri: (folderName: string): URI | undefined => { - const found = folders.filter(f => f.name === folderName); - if (found && found.length > 0) { - return found[0].uri; - } - return undefined; - }, - getWorkspaceFolderCount: (): number => { - return folders.length; - }, - getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { - return configurationService.getConfiguration(undefined, folderUri).get(section); - }, - getAppRoot: (): string | undefined => { - return process.cwd(); - }, - getExecPath: (): string | undefined => { - return process.env['VSCODE_EXEC_PATH']; - }, - getFilePath: (): string | undefined => { - const activeUri = getActiveUri(); - if (activeUri) { - return path.normalize(activeUri.fsPath); - } - return undefined; - }, - getWorkspaceFolderPathForFile: (): string | undefined => { - if (workspaceService) { - const activeUri = getActiveUri(); - if (activeUri) { - const ws = workspaceService.getWorkspaceFolder(activeUri); - if (ws) { - return path.normalize(ws.uri.fsPath); - } - } - } - return undefined; - }, - getSelectedText: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor && !activeEditor.selection.isEmpty) { - return activeEditor.document.getText(activeEditor.selection); - } - } - return undefined; - }, - getLineNumber: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return String(activeEditor.selection.end.line + 1); - } - } - return undefined; - } - }, undefined, userHome ? Promise.resolve(userHome) : undefined, Promise.resolve(process.env)); - } -} - interface ConfigProviderTuple { type: string; handle: number; @@ -1108,14 +1006,10 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, - @IExtHostEditorTabs editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); - } - - protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index b3ff5c0c2b4..01ddef79296 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -229,6 +229,15 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return false; } + public async getExtension(extensionId: string): Promise { + const ext = await this._mainThreadExtensionsProxy.$getExtension(extensionId); + return ext && { + ...ext, + identifier: new ExtensionIdentifier(ext.identifier.value), + extensionLocation: URI.revive(ext.extensionLocation), + }; + } + private _activateByEvent(activationEvent: string, startup: boolean): Promise { return this._activator.activateByEvent(activationEvent, startup); } @@ -885,6 +894,7 @@ export const IExtHostExtensionService = createDecorator; + getExtension(extensionId: string): Promise; isActivated(extensionId: ExtensionIdentifier): boolean; activateByIdWithErrors(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; deactivateAll(): Promise; diff --git a/src/vs/workbench/api/common/extHostVariableResolverService.ts b/src/vs/workbench/api/common/extHostVariableResolverService.ts new file mode 100644 index 00000000000..a992dd05f77 --- /dev/null +++ b/src/vs/workbench/api/common/extHostVariableResolverService.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as path from 'vs/base/common/path'; +import * as process from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TextDiffTabInput, TextTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import * as vscode from 'vscode'; +import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration'; + +export interface IExtHostVariableResolverProvider { + readonly _serviceBrand: undefined; + getResolver(): Promise; +} + +export const IExtHostVariableResolverProvider = createDecorator('IExtHostVariableResolverProvider'); + +interface DynamicContext { + folders: vscode.WorkspaceFolder[]; +} + +class ExtHostVariableResolverService extends AbstractVariableResolverService { + + constructor( + extensionService: IExtHostExtensionService, + workspaceService: IExtHostWorkspace, + editorService: IExtHostDocumentsAndEditors, + editorTabs: IExtHostEditorTabs, + configProvider: ExtHostConfigProvider, + context: DynamicContext, + homeDir: string | undefined, + ) { + function getActiveUri(): URI | undefined { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return activeEditor.document.uri; + } + const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab; + if (activeTab !== undefined) { + // Resolve a resource from the tab + if (activeTab.kind instanceof TextDiffTabInput || activeTab.kind instanceof NotebookDiffEditorTabInput) { + return activeTab.kind.modified; + } else if (activeTab.kind instanceof TextTabInput || activeTab.kind instanceof NotebookEditorTabInput || activeTab.kind instanceof CustomEditorTabInput) { + return activeTab.kind.uri; + } + } + } + return undefined; + } + + super({ + getFolderUri: (folderName: string): URI | undefined => { + const found = context.folders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return context.folders.length; + }, + getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { + return configProvider.getConfiguration(undefined, folderUri).get(section); + }, + getAppRoot: (): string | undefined => { + return process.cwd(); + }, + getExecPath: (): string | undefined => { + return process.env['VSCODE_EXEC_PATH']; + }, + getFilePath: (): string | undefined => { + const activeUri = getActiveUri(); + if (activeUri) { + return path.normalize(activeUri.fsPath); + } + return undefined; + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + if (workspaceService) { + const activeUri = getActiveUri(); + if (activeUri) { + const ws = workspaceService.getWorkspaceFolder(activeUri); + if (ws) { + return path.normalize(ws.uri.fsPath); + } + } + } + return undefined; + }, + getSelectedText: (): string | undefined => { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor && !activeEditor.selection.isEmpty) { + return activeEditor.document.getText(activeEditor.selection); + } + } + return undefined; + }, + getLineNumber: (): string | undefined => { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return String(activeEditor.selection.end.line + 1); + } + } + return undefined; + }, + getExtension: (id) => { + return extensionService.getExtension(id); + }, + }, undefined, homeDir ? Promise.resolve(homeDir) : undefined, Promise.resolve(process.env)); + } +} + +export class ExtHostVariableResolverProviderService extends Disposable implements IExtHostVariableResolverProvider { + declare readonly _serviceBrand: undefined; + + private _resolver = new Lazy(async () => { + const configProvider = await this.configurationService.getConfigProvider(); + const folders = await this.workspaceService.getWorkspaceFolders2() || []; + + const dynamic: DynamicContext = { folders }; + this._register(this.workspaceService.onDidChangeWorkspace(async e => { + dynamic.folders = await this.workspaceService.getWorkspaceFolders2() || []; + })); + + return new ExtHostVariableResolverService( + this.extensionService, + this.workspaceService, + this.editorService, + this.editorTabs, + configProvider, + dynamic, + this.homeDir(), + ); + }); + + constructor( + @IExtHostExtensionService private readonly extensionService: IExtHostExtensionService, + @IExtHostWorkspace private readonly workspaceService: IExtHostWorkspace, + @IExtHostDocumentsAndEditors private readonly editorService: IExtHostDocumentsAndEditors, + @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration, + @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs, + ) { + super(); + } + + public getResolver(): Promise { + return this._resolver.getValue(); + } + + protected homeDir(): string | undefined { + return undefined; + } +} diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index 5bc2abbca64..0cbd7e57c22 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -20,6 +20,8 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths'; import { ExtHostLoggerService } from 'vs/workbench/api/node/extHostLoggerService'; import { ILoggerService } from 'vs/platform/log/common/log'; +import { NodeExtHostVariableResolverProviderService } from 'vs/workbench/api/node/extHostVariableResolverService'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; // ######################################################################### // ### ### @@ -36,3 +38,4 @@ registerSingleton(IExtHostSearch, NativeExtHostSearch); registerSingleton(IExtHostTask, ExtHostTask); registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostVariableResolverProvider, NodeExtHostVariableResolverProviderService); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index b235b34a8fc..d2127334678 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -3,31 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import type * as vscode from 'vscode'; -import { homedir } from 'os'; +import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; -import { DebugAdapterExecutable, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; -import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; -import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IAdapterDescriptor } from 'vs/workbench/contrib/debug/common/debug'; -import { IExtHostConfiguration, ExtHostConfigProvider } from '../common/extHostConfiguration'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { ExtHostDebugServiceBase, ExtHostDebugSession, ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; +import * as nls from 'vs/nls'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; -import { hasChildProcesses, prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; +import { ExtHostDebugServiceBase, ExtHostDebugSession } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; -import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; +import { DebugAdapterExecutable, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; +import { IAdapterDescriptor } from 'vs/workbench/contrib/debug/common/debug'; +import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; +import { hasChildProcesses, prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import type * as vscode from 'vscode'; +import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -40,12 +38,12 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostTerminalService private _terminalService: IExtHostTerminalService, - @IExtHostEditorTabs editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -154,10 +152,6 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } return super.$runInTerminal(args, sessionId); } - - protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs, this._workspaceService, homedir()); - } } let externalTerminalService: IExternalTerminalService | undefined = undefined; diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index c47e3203461..7afdfc40ea0 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -11,7 +11,6 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; -import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IWorkspaceFolder, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -23,13 +22,11 @@ import { ExtHostTaskBase, TaskHandleDTO, TaskDTO, CustomExecutionDTO, HandlerDat import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import * as resources from 'vs/base/common/resources'; import { homedir } from 'os'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; export class ExtHostTask extends ExtHostTaskBase { - private _variableResolver: ExtHostVariableResolverService | undefined; - constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -39,7 +36,7 @@ export class ExtHostTask extends ExtHostTaskBase { @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @ILogService logService: ILogService, @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService, - @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider private readonly variableResolver: IExtHostVariableResolverProvider, ) { super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService); if (initData.remote.isRemote && initData.remote.authority) { @@ -128,14 +125,6 @@ export class ExtHostTask extends ExtHostTaskBase { return resolvedTaskDTO; } - private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise { - if (this._variableResolver === undefined) { - const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.editorTabs, this.workspaceService, homedir()); - } - return this._variableResolver; - } - private async getAFolder(workspaceFolders: vscode.WorkspaceFolder[] | undefined): Promise { let folder = (workspaceFolders && workspaceFolders.length > 0) ? workspaceFolders[0] : undefined; if (!folder) { @@ -161,7 +150,7 @@ export class ExtHostTask extends ExtHostTaskBase { const workspaceFolder = await this._workspaceProvider.resolveWorkspaceFolder(uri); const workspaceFolders = (await this._workspaceProvider.getWorkspaceFolders2()) ?? []; - const resolver = await this.getVariableResolver(workspaceFolders); + const resolver = await this.variableResolver.getResolver(); const ws: IWorkspaceFolder = workspaceFolder ? { uri: workspaceFolder.uri, name: workspaceFolder.name, diff --git a/src/vs/workbench/api/node/extHostVariableResolverService.ts b/src/vs/workbench/api/node/extHostVariableResolverService.ts new file mode 100644 index 00000000000..c6439fd4e03 --- /dev/null +++ b/src/vs/workbench/api/node/extHostVariableResolverService.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { homedir } from 'os'; +import { ExtHostVariableResolverProviderService } from 'vs/workbench/api/common/extHostVariableResolverService'; + +export class NodeExtHostVariableResolverProviderService extends ExtHostVariableResolverProviderService { + protected override homeDir(): string | undefined { + return homedir(); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 5e749dcac5b..6c09333437c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -388,7 +388,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); + const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); @@ -398,7 +398,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // info widget. While technically these could differ due to the slight change of a race // condition, the chance is minimal plus the impact on the user is also not that great // if it happens - it's not worth adding plumbing to sync back the resolved collection. - this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); + await this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); if (this._extEnvironmentVariableCollection.map.size > 0) { this.environmentVariableInfo = new EnvironmentVariableInfoChangesActive(this._extEnvironmentVariableCollection); this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo); @@ -423,7 +423,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); - const initialCwd = terminalEnvironment.getCwd( + const initialCwd = await terminalEnvironment.getCwd( shellLaunchConfig, userHome, variableResolver, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 5885b71b3a3..0849f6676b2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -355,25 +355,23 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro const env = await this._context.getEnvironment(options.remoteAuthority); const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(options.remoteAuthority ? Schemas.vscodeRemote : Schemas.file); const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - profile.path = this._resolveVariables(profile.path, env, lastActiveWorkspace); + profile.path = await this._resolveVariables(profile.path, env, lastActiveWorkspace); // Resolve args variables if (profile.args) { if (typeof profile.args === 'string') { - profile.args = this._resolveVariables(profile.args, env, lastActiveWorkspace); + profile.args = await this._resolveVariables(profile.args, env, lastActiveWorkspace); } else { - for (let i = 0; i < profile.args.length; i++) { - profile.args[i] = this._resolveVariables(profile.args[i], env, lastActiveWorkspace); - } + profile.args = await Promise.all(profile.args.map(arg => this._resolveVariables(arg, env, lastActiveWorkspace))); } } return profile; } - private _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { + private async _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { try { - value = this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); + value = await this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); } catch (e) { this._logService.error(`Could not resolve shell`, e); } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index c307e138dd4..1f105bfaa94 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -7,6 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; export const IEnvironmentVariableService = createDecorator('environmentVariableService'); @@ -51,7 +52,7 @@ export interface IMergedEnvironmentVariableCollection { * @param variableResolver An optional function to use to resolve variables within the * environment values. */ - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: (str: string) => string): void; + applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise; /** * Generates a diff of this connection against another. Returns undefined if the collections are diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts index a64ef609d29..e67f607c506 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts @@ -5,6 +5,7 @@ import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { EnvironmentVariableMutatorType, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { readonly map: Map = new Map(); @@ -41,16 +42,16 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa }); } - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: (str: string) => string): void { + async applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise { let lowerToActualVariableNames: { [lowerKey: string]: string | undefined } | undefined; if (isWindows) { lowerToActualVariableNames = {}; Object.keys(env).forEach(e => lowerToActualVariableNames![e.toLowerCase()] = e); } - this.map.forEach((mutators, variable) => { + for (const [variable, mutators] of this.map) { const actualVariable = isWindows ? lowerToActualVariableNames![variable.toLowerCase()] || variable : variable; - mutators.forEach(mutator => { - const value = variableResolver ? variableResolver(mutator.value) : mutator.value; + for (const mutator of mutators) { + const value = variableResolver ? await variableResolver(mutator.value) : mutator.value; switch (mutator.type) { case EnvironmentVariableMutatorType.Append: env[actualVariable] = (env[actualVariable] || '') + value; @@ -62,8 +63,8 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa env[actualVariable] = value; break; } - }); - }); + } + } } diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined { diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index b1922c5cd52..80041aeb869 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -78,17 +78,17 @@ function mergeNonNullKeys(env: IProcessEnvironment, other: ITerminalEnvironment } } -function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): ITerminalEnvironment { - Object.keys(env).forEach((key) => { - const value = env[key]; +async function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): Promise { + await Promise.all(Object.entries(env).map(async ([key, value]) => { if (typeof value === 'string') { try { - env[key] = variableResolver(value); + env[key] = await variableResolver(value); } catch (e) { env[key] = value; } } - }); + })); + return env; } @@ -179,17 +179,17 @@ export function getLangEnvVariable(locale?: string): string { return parts.join('_') + '.UTF-8'; } -export function getCwd( +export async function getCwd( shell: IShellLaunchConfig, userHome: string | undefined, variableResolver: VariableResolver | undefined, root: Uri | undefined, customCwd: string | undefined, logService?: ILogService -): string { +): Promise { if (shell.cwd) { const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; - const resolved = _resolveCwd(unresolved, variableResolver); + const resolved = await _resolveCwd(unresolved, variableResolver); return _sanitizeCwd(resolved || unresolved); } @@ -197,7 +197,7 @@ export function getCwd( if (!shell.ignoreConfigurationCwd && customCwd) { if (variableResolver) { - customCwd = _resolveCwd(customCwd, variableResolver, logService); + customCwd = await _resolveCwd(customCwd, variableResolver, logService); } if (customCwd) { if (path.isAbsolute(customCwd)) { @@ -216,10 +216,10 @@ export function getCwd( return _sanitizeCwd(cwd); } -function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): string | undefined { +async function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): Promise { if (variableResolver) { try { - return variableResolver(cwd); + return await variableResolver(cwd); } catch (e) { logService?.error('Could not resolve terminal cwd', e); return undefined; @@ -251,7 +251,7 @@ export type TerminalShellArgsSetting = ( | TerminalSettingId.ShellArgsLinux ); -export type VariableResolver = (str: string) => string; +export type VariableResolver = (str: string) => Promise; export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | undefined, env: IProcessEnvironment, configurationResolverService: IConfigurationResolverService | undefined): VariableResolver | undefined { if (!configurationResolverService) { @@ -263,7 +263,7 @@ export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | u /** * @deprecated Use ITerminalProfileResolverService */ -export function getDefaultShell( +export async function getDefaultShell( fetchSetting: (key: TerminalShellSetting) => string | undefined, defaultShell: string, isWoW64: boolean, @@ -272,7 +272,7 @@ export function getDefaultShell( logService: ILogService, useAutomationShell: boolean, platformOverride: Platform = platform -): string { +): Promise { let maybeExecutable: string | undefined; if (useAutomationShell) { // If automationShell is specified, this should override the normal setting @@ -300,7 +300,7 @@ export function getDefaultShell( if (variableResolver) { try { - executable = variableResolver(executable); + executable = await variableResolver(executable); } catch (e) { logService.error(`Could not resolve shell`, e); } @@ -312,13 +312,13 @@ export function getDefaultShell( /** * @deprecated Use ITerminalProfileResolverService */ -export function getDefaultShellArgs( +export async function getDefaultShellArgs( fetchSetting: (key: TerminalShellSetting | TerminalShellArgsSetting) => string | string[] | undefined, useAutomationShell: boolean, variableResolver: VariableResolver | undefined, logService: ILogService, platformOverride: Platform = platform, -): string | string[] { +): Promise { if (useAutomationShell) { if (!!getShellSetting(fetchSetting, 'automationShell', platformOverride)) { return []; @@ -331,13 +331,13 @@ export function getDefaultShellArgs( return []; } if (typeof args === 'string' && platformOverride === Platform.Windows) { - return variableResolver ? variableResolver(args) : args; + return variableResolver ? await variableResolver(args) : args; } if (variableResolver) { const resolvedArgs: string[] = []; for (const arg of args) { try { - resolvedArgs.push(variableResolver(arg)); + resolvedArgs.push(await variableResolver(arg)); } catch (e) { logService.error(`Could not resolve ${TerminalSettingPrefix.ShellArgs}${platformKey}`, e); resolvedArgs.push(arg); @@ -357,14 +357,14 @@ function getShellSetting( return fetchSetting(`terminal.integrated.${type}.${platformKey}`); } -export function createTerminalEnvironment( +export async function createTerminalEnvironment( shellLaunchConfig: IShellLaunchConfig, envFromConfig: ITerminalEnvironment | undefined, variableResolver: VariableResolver | undefined, version: string | undefined, detectLocale: 'auto' | 'off' | 'on', baseEnv: IProcessEnvironment -): IProcessEnvironment { +): Promise { // Create a terminal environment based on settings, launch config and permissions const env: IProcessEnvironment = {}; if (shellLaunchConfig.strictEnv) { @@ -379,10 +379,10 @@ export function createTerminalEnvironment( // Resolve env vars from config and shell if (variableResolver) { if (allowedEnvFromConfig) { - resolveConfigurationVariables(variableResolver, allowedEnvFromConfig); + await resolveConfigurationVariables(variableResolver, allowedEnvFromConfig); } if (shellLaunchConfig.env) { - resolveConfigurationVariables(variableResolver, shellLaunchConfig.env); + await resolveConfigurationVariables(variableResolver, shellLaunchConfig.env); } } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index c38b20757e8..b2f01c3e853 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -250,9 +250,9 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); + const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { - this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); + await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); } return env; } diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts index 7b0b6594d4a..40c086a7dcc 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts @@ -78,7 +78,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); suite('applyToProcessEnvironment', () => { - test('should apply the collection to an environment', () => { + test('should apply the collection to an environment', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -93,7 +93,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'bar', C: 'baz' }; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'barb', @@ -101,7 +101,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); - test('should apply the collection to environment entries with no values', () => { + test('should apply the collection to environment entries with no values', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -112,7 +112,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }] ])); const env: IProcessEnvironment = {}; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'b', @@ -120,7 +120,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); - test('should apply to variable case insensitively on Windows only', () => { + test('should apply to variable case insensitively on Windows only', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -135,7 +135,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'B', C: 'C' }; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); if (isWindows) { deepStrictEqual(env, { A: 'a', diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts index ed3269a6b83..dd391ea487d 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts @@ -87,7 +87,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { ]); }); - test('should correctly apply the environment values from multiple extension contributions in the correct order', () => { + test('should correctly apply the environment values from multiple extension contributions in the correct order', async () => { const collection1 = new Map(); const collection2 = new Map(); const collection3 = new Map(); @@ -109,7 +109,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { // Verify the entries get applied to the environment as expected const env: IProcessEnvironment = { A: 'foo' }; - environmentVariableService.mergedCollection.applyToProcessEnvironment(env); + await environmentVariableService.mergedCollection.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a2:a3:a1' }); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index ca148531c12..b3918029cd0 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -180,66 +180,66 @@ suite('Workbench - TerminalEnvironment', () => { strictEqual(Uri.file(a).fsPath, Uri.file(b).fsPath); } - test('should default to userHome for an empty workspace', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, undefined), '/userHome/'); + test('should default to userHome for an empty workspace', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, undefined), '/userHome/'); }); - test('should use to the workspace if it exists', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/foo'), undefined), '/foo'); + test('should use to the workspace if it exists', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/foo'), undefined), '/foo'); }); - test('should use an absolute custom cwd as is', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '/foo'), '/foo'); + test('should use an absolute custom cwd as is', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '/foo'), '/foo'); }); - test('should normalize a relative custom cwd against the workspace path', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), 'foo'), '/bar/foo'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), './foo'), '/bar/foo'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), '../foo'), '/foo'); + test('should normalize a relative custom cwd against the workspace path', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), 'foo'), '/bar/foo'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), './foo'), '/bar/foo'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), '../foo'), '/foo'); }); - test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, 'foo'), '/userHome/'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, './foo'), '/userHome/'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '../foo'), '/userHome/'); + test('should fall back for relative a custom cwd that doesn\'t have a workspace', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, 'foo'), '/userHome/'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, './foo'), '/userHome/'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '../foo'), '/userHome/'); }); - test('should ignore custom cwd when told to ignore', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, Uri.file('/bar'), '/foo'), '/bar'); + test('should ignore custom cwd when told to ignore', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, Uri.file('/bar'), '/foo'), '/bar'); }); }); suite('getDefaultShell', () => { - test('should change Sysnative to System32 in non-WoW64 systems', () => { - const shell = getDefaultShell(key => { + test('should change Sysnative to System32 in non-WoW64 systems', async () => { + const shell = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell, 'C:\\Windows\\System32\\cmd.exe'); }); - test('should not change Sysnative to System32 in WoW64 systems', () => { - const shell = getDefaultShell(key => { + test('should not change Sysnative to System32 in WoW64 systems', async () => { + const shell = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; }, 'DEFAULT', true, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell, 'C:\\Windows\\Sysnative\\cmd.exe'); }); - test('should use automationShell when specified', () => { - const shell1 = getDefaultShell(key => { + test('should use automationShell when specified', async () => { + const shell1 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': undefined } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell1, 'shell', 'automationShell was false'); - const shell2 = getDefaultShell(key => { + const shell2 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': undefined } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, Platform.Windows); strictEqual(shell2, 'shell', 'automationShell was true'); - const shell3 = getDefaultShell(key => { + const shell3 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': 'automationShell' diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts index f6f0d6fa353..b4f3c050e7e 100644 --- a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -19,6 +19,7 @@ import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/com import { IProcessEnvironment } from 'vs/base/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { @@ -36,7 +37,8 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR private readonly workspaceContextService: IWorkspaceContextService, private readonly quickInputService: IQuickInputService, private readonly labelService: ILabelService, - private readonly pathService: IPathService + private readonly pathService: IPathService, + extensionService: IExtensionService, ) { super({ getFolderUri: (folderName: string): uri | undefined => { @@ -109,7 +111,10 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR } } return undefined; - } + }, + getExtension: id => { + return extensionService.getExtension(id); + }, }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); } diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index c4de8509832..4a7d00a5b4d 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; export class ConfigurationResolverService extends BaseConfigurationResolverService { @@ -23,11 +24,12 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IExtensionService extensionService: IExtensionService, ) { super({ getAppRoot: () => undefined, getExecPath: () => undefined }, Promise.resolve(Object.create(null)), editorService, configurationService, - commandService, workspaceContextService, quickInputService, labelService, pathService); + commandService, workspaceContextService, quickInputService, labelService, pathService, extensionService); } } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index d8ba161425b..37a44d15196 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -14,7 +14,7 @@ export const IConfigurationResolverService = createDecorator; resolveAsync(folder: IWorkspaceFolder | undefined, value: string): Promise; resolveAsync(folder: IWorkspaceFolder | undefined, value: string[]): Promise; diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index a75a3c77ef7..7e557d33120 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -15,6 +15,7 @@ import { URI as uri } from 'vs/base/common/uri'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; +import { replaceAsync } from 'vs/base/common/strings'; export interface IVariableResolveContext { getFolderUri(folderName: string): uri | undefined; @@ -26,6 +27,7 @@ export interface IVariableResolveContext { getWorkspaceFolderPathForFile?(): string | undefined; getSelectedText(): string | undefined; getLineNumber(): string | undefined; + getExtension(id: string): Promise<{ readonly extensionLocation: uri } | undefined>; } type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined }; @@ -66,7 +68,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe return envVariables; } - public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): string { + public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): Promise { return this.recursiveResolve({ env: this.prepareEnv(environment), userHome: undefined }, root ? root.uri : undefined, value); } @@ -133,52 +135,53 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe } } - private recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): any { + private async recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): Promise { if (types.isString(value)) { return this.resolveString(environment, folderUri, value, commandValueMapping, resolvedVariables); } else if (types.isArray(value)) { - return value.map(s => this.recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables)); + return Promise.all(value.map(s => this.recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables))); } else if (types.isObject(value)) { let result: IStringDictionary | string[]> = Object.create(null); - Object.keys(value).forEach(key => { - const replaced = this.resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); - result[replaced] = this.recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables); - }); + const replaced = await Promise.all(Object.keys(value).map(async key => { + const replaced = await this.resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); + return [replaced, await this.recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables)] as const; + })); + // two step process to preserve object key order + for (const [key, value] of replaced) { + result[key] = value; + } return result; } return value; } - private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary | undefined, resolvedVariables?: Map): string { - + private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary | undefined, resolvedVariables?: Map): Promise { // loop through all variables occurrences in 'value' - const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { + return replaceAsync(value, AbstractVariableResolverService.VARIABLE_REGEXP, async (match: string, variable: string) => { // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. if (variable.includes(AbstractVariableResolverService.VARIABLE_LHS)) { return match; } - let resolvedValue = this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); + let resolvedValue = await this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); if (resolvedVariables) { resolvedVariables.set(variable, resolvedValue); } if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(AbstractVariableResolverService.VARIABLE_REGEXP)) { - resolvedValue = this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); + resolvedValue = await this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); } return resolvedValue; }); - - return replaced; } private fsPath(displayUri: uri): string { return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath; } - private evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary | undefined): string { + private async evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary | undefined): Promise { // try to separate variable arguments from variable name let argument: string | undefined; @@ -268,6 +271,16 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe case 'input': return this.resolveFromMap(match, argument, commandValueMapping, 'input'); + case 'extensionInstallFolder': + if (argument) { + const ext = await this._context.getExtension(argument); + if (!ext) { + throw new Error(localize('extensionNotInstalled', "Variable {0} can not be resolved because the extension {1} is not installed.", match, argument)); + } + return this.fsPath(ext.extensionLocation); + } + throw new Error(localize('missingExtensionName', "Variable {0} can not be resolved because no extension name is given.", match)); + default: { switch (variable) { diff --git a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts index f4fcf389c87..14a385e3e06 100644 --- a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts @@ -15,6 +15,7 @@ import { BaseConfigurationResolverService } from 'vs/workbench/services/configur import { ILabelService } from 'vs/platform/label/common/label'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class ConfigurationResolverService extends BaseConfigurationResolverService { @@ -27,7 +28,8 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, @IShellEnvironmentService shellEnvironmentService: IShellEnvironmentService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IExtensionService extensionService: IExtensionService, ) { super({ getAppRoot: (): string | undefined => { @@ -35,9 +37,9 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi }, getExecPath: (): string | undefined => { return environmentService.execPath; - } + }, }, shellEnvironmentService.getShellEnv(), editorService, configurationService, commandService, - workspaceContextService, quickInputService, labelService, pathService); + workspaceContextService, quickInputService, labelService, pathService, extensionService); } } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index ed179a4847b..b25e3149005 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { stub } from 'sinon'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -16,15 +17,17 @@ import { EditorType } from 'vs/editor/common/editorCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { IWorkspace, IWorkspaceFolder, IWorkspaceIdentifier, Workspace } from 'vs/platform/workspace/common/workspace'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { TestEditorService, TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { TestContextService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestExtensionService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativeWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; const mockLineNumber = 10; @@ -68,6 +71,7 @@ suite('Configuration Resolver Service', () => { let quickInputService: TestQuickInputService; let labelService: MockLabelService; let pathService: MockPathService; + let extensionService: IExtensionService; setup(() => { mockCommandService = new MockCommandService(); @@ -76,9 +80,10 @@ suite('Configuration Resolver Service', () => { environmentService = new MockWorkbenchEnvironmentService(envVariables); labelService = new MockLabelService(); pathService = new MockPathService(); + extensionService = new TestExtensionService(); containingWorkspace = testWorkspace(uri.parse('file:///VSCode/workspaceLocation')); workspace = containingWorkspace.folders[0]; - configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService); + configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService, extensionService); }); teardown(() => { @@ -185,6 +190,13 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${env:key1} ${env:key1${env:key2}}'), 'Value for key1 ${env:key1${env:key2}}'); }); + test('supports extensionDir', async () => { + const getExtension = stub(extensionService, 'getExtension'); + getExtension.withArgs('publisher.extId').returns(Promise.resolve({ extensionLocation: uri.file('/some/path') } as IExtensionDescription)); + + assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${extensionInstallFolder:publisher.extId}'), uri.file('/some/path').fsPath); + }); + // test('substitute keys and values in object', () => { // const myObject = { // '${workspaceRootFolderName}': '${lineNumber}', @@ -217,7 +229,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -228,7 +240,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(undefined, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -245,7 +257,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); @@ -262,7 +274,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -283,7 +295,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2'); } else { @@ -317,7 +329,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); @@ -327,7 +339,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); @@ -340,7 +352,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env} xyz')); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env:} xyz')); @@ -632,13 +644,13 @@ suite('Configuration Resolver Service', () => { }); }); - test('resolveWithEnvironment', () => { + test('resolveWithEnvironment', async () => { const env = { 'VAR_1': 'VAL_1', 'VAR_2': 'VAL_2' }; const configuration = 'echo ${env:VAR_1}${env:VAR_2}'; - const resolvedResult = configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); + const resolvedResult = await configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); assert.deepStrictEqual(resolvedResult, 'echo VAL_1VAL_2'); }); });