variables: allow resolving extensionDir (#146274)

* variables: allow resolving `extensionDir`

This allows us to fix https://github.com/microsoft/vscode-remote-release/issues/5516#issuecomment-911597917

It enables a new replacement in the format `${extensionDir:<id>}` which
will expand to the filesystem path where the extension is stored. This
involved churn, since now resolution is always synchronous (where before
the terminal took a synchronous-only path.)

Additionally, changes were needed to inject this information in the
variable resolver. As part of this I made the extension host resolver
(used by debug and tasks) its own extension host service.

* fixup! preserve object key order in resolution, add extensionDir support

* fixup! address pr comments

* fixup! address pr comments

* fixup! address pr comments

* config: fix config replacement only working for first variable per line

* fixup! fix unit tests
This commit is contained in:
Connor Peet 2022-04-07 08:06:31 -07:00 committed by GitHub
parent 0fba8139ce
commit 0de44f9786
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 450 additions and 298 deletions

View File

@ -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
}));
}

View File

@ -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<string>): Promise<string> {
let parts: (string | Promise<string>)[] = [];
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;

View File

@ -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');
});
});

View File

@ -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<string, IEnvironmentVariableCollection>(entries);
const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections);
mergedCollection.applyToProcessEnvironment(env);
await mergedCollection.applyToProcessEnvironment(env, variableResolver);
}
// Fork the process and listen for messages

View File

@ -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);

View File

@ -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<void> {
return this._internalExtensionService._activateById(extensionId, reason);
}

View File

@ -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);

View File

@ -1107,6 +1107,7 @@ export interface MainThreadTaskShape extends IDisposable {
}
export interface MainThreadExtensionServiceShape extends IDisposable {
$getExtension(extensionId: string): Promise<Dto<IExtensionDescription> | undefined>;
$activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void>;
$onWillActivateExtension(extensionId: ExtensionIdentifier): Promise<void>;
$onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void;

View File

@ -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>('IExtHostDebugService');
@ -101,17 +94,15 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E
private _debugAdapters: Map<number, IDebugAdapter>;
private _debugAdaptersTrackers: Map<number, vscode.DebugAdapterTracker>;
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<IConfig> {
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<string>(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);
}
}

View File

@ -229,6 +229,15 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
return false;
}
public async getExtension(extensionId: string): Promise<IExtensionDescription | undefined> {
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<void> {
return this._activator.activateByEvent(activationEvent, startup);
}
@ -885,6 +894,7 @@ export const IExtHostExtensionService = createDecorator<IExtHostExtensionService
export interface IExtHostExtensionService extends AbstractExtHostExtensionService {
readonly _serviceBrand: undefined;
initialize(): Promise<void>;
getExtension(extensionId: string): Promise<IExtensionDescription | undefined>;
isActivated(extensionId: ExtensionIdentifier): boolean;
activateByIdWithErrors(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void>;
deactivateAll(): Promise<void>;

View File

@ -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<IConfigurationResolverService>;
}
export const IExtHostVariableResolverProvider = createDecorator<IExtHostVariableResolverProvider>('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<string>(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<IConfigurationResolverService> {
return this._resolver.getValue();
}
protected homeDir(): string | undefined {
return undefined;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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<ExtHostVariableResolverService> {
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<IWorkspaceFolder> {
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,

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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<IEnvironmentVariableService>('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<void>;
/**
* Generates a diff of this connection against another. Returns undefined if the collections are

View File

@ -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<string, IExtensionOwnedEnvironmentVariableMutator[]> = 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<void> {
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 {

View File

@ -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<ITerminalEnvironment> {
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<string> {
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<string | undefined> {
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<string>;
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<string> {
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<string | string[]> {
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(<TerminalShellSetting>`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<IProcessEnvironment> {
// 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);
}
}

View File

@ -250,9 +250,9 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke
const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux');
const envFromConfigValue = this._configurationService.getValue<ITerminalEnvironment | undefined>(`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;
}

View File

@ -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',

View File

@ -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<string, IEnvironmentVariableMutator>();
const collection2 = new Map<string, IEnvironmentVariableMutator>();
const collection3 = new Map<string, IEnvironmentVariableMutator>();
@ -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' });
});
});

View File

@ -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'

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -14,7 +14,7 @@ export const IConfigurationResolverService = createDecorator<IConfigurationResol
export interface IConfigurationResolverService {
readonly _serviceBrand: undefined;
resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolder | undefined, value: string): string;
resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolder | undefined, value: string): Promise<string>;
resolveAsync(folder: IWorkspaceFolder | undefined, value: string): Promise<string>;
resolveAsync(folder: IWorkspaceFolder | undefined, value: string[]): Promise<string[]>;

View File

@ -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<string> {
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<string>, resolvedVariables?: Map<string, string>): any {
private async recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): Promise<any> {
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 | IStringDictionary<string> | 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<string> | undefined, resolvedVariables?: Map<string, string>): string {
private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary<string> | undefined, resolvedVariables?: Map<string, string>): Promise<string> {
// 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<string> | undefined): string {
private async evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary<string> | undefined): Promise<string> {
// 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) {

View File

@ -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);
}
}

View File

@ -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');
});
});