diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 37486e3a92b..d01e7dc7533 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -25,13 +25,12 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumen import { IAdapterExecutable, ITerminalSettings, IDebuggerContribution, IConfig, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; import { getTerminalLauncher, hasChildprocesses, prepareCommand } from 'vs/workbench/parts/debug/node/terminals'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { ExtHostConfiguration } from './extHostConfiguration'; import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/common/debugUtils'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; export class ExtHostDebugService implements ExtHostDebugServiceShape { @@ -176,7 +175,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { throw new Error('Not implemented'); } }; - return asWinJsPromise(token => DebugAdapter.substituteVariables(ws, config, this._variableResolver)); + return asWinJsPromise(token => this._variableResolver.resolveAny(ws, config)); } public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null, debugPort: number): TPromise { @@ -588,15 +587,12 @@ export class ExtHostDebugConsole implements vscode.DebugConsole { } } -export class ExtHostVariableResolverService implements IConfigurationResolverService { +export class ExtHostVariableResolverService extends AbstractVariableResolverService { - _serviceBrand: any; - _variableResolver: VariableResolver; - - constructor(workspace: ExtHostWorkspace, editors: ExtHostDocumentsAndEditors, configuration: ExtHostConfiguration) { - this._variableResolver = new VariableResolver({ + constructor(workspaceService: ExtHostWorkspace, editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfiguration) { + super({ getFolderUri: (folderName: string): URI => { - const folders = workspace.getWorkspaceFolders(); + const folders = workspaceService.getWorkspaceFolders(); const found = folders.filter(f => f.name === folderName); if (found && found.length > 0) { return found[0].uri; @@ -604,16 +600,16 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer return undefined; }, getWorkspaceFolderCount: (): number => { - return workspace.getWorkspaceFolders().length; + return workspaceService.getWorkspaceFolders().length; }, getConfigurationValue: (folderUri: URI, section: string) => { - return configuration.getConfiguration(undefined, folderUri).get(section); + return configurationService.getConfiguration(undefined, folderUri).get(section); }, getExecPath: (): string | undefined => { return undefined; // does not exist in EH }, getFilePath: (): string | undefined => { - const activeEditor = editors.activeEditor(); + const activeEditor = editorService.activeEditor(); if (activeEditor) { const resource = activeEditor.document.uri; if (resource.scheme === Schemas.file) { @@ -623,38 +619,19 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer return undefined; }, getSelectedText: (): string | undefined => { - const activeEditor = editors.activeEditor(); + const activeEditor = editorService.activeEditor(); if (activeEditor && !activeEditor.selection.isEmpty) { return activeEditor.document.getText(activeEditor.selection); } return undefined; }, getLineNumber: (): string => { - const activeEditor = editors.activeEditor(); + const activeEditor = editorService.activeEditor(); if (activeEditor) { return String(activeEditor.selection.end.line + 1); } return undefined; } - }, process.env); - } - - public resolve(root: IWorkspaceFolder, value: string): string; - public resolve(root: IWorkspaceFolder, value: string[]): string[]; - public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; - public resolve(root: IWorkspaceFolder, value: any): any { - return this._variableResolver.resolveAny(root ? root.uri : undefined, value); - } - - public resolveAny(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary): T { - return this._variableResolver.resolveAny(root ? root.uri : undefined, value, commandMapping); - } - - public executeCommandVariables(configuration: any, variables: IStringDictionary): TPromise> { - throw new Error('findAndExecuteCommandVariables not implemented.'); - } - - public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise { - throw new Error('resolveWithCommands not implemented.'); + }); } } diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index befbb0fd4e8..286a6a741f2 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -18,10 +18,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; -import { IDebugAdapter, IAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IConfig } from 'vs/workbench/parts/debug/common/debug'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { IDebugAdapter, IAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution } from 'vs/workbench/parts/debug/common/debug'; /** * Abstract implementation of the low level API for a debug adapter. @@ -440,26 +437,4 @@ export class DebugAdapter extends StreamDebugAdapter { }; } } - - static substituteVariables(workspaceFolder: IWorkspaceFolder, config: IConfig, resolverService: IConfigurationResolverService, commandValueMapping?: IStringDictionary): IConfig { - - const result = objects.deepClone(config) as IConfig; - - // hoist platform specific attributes to top level - if (platform.isWindows && result.windows) { - Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); - } else if (platform.isMacintosh && result.osx) { - Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); - } else if (platform.isLinux && result.linux) { - Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); - } - - // delete all platform specific sections - delete result.windows; - delete result.osx; - delete result.linux; - - // substitute all variables in string values - return resolverService.resolveAny(workspaceFolder, result, commandValueMapping); - } } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index 4b7eaf8f931..e6ae183d5ec 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -13,9 +13,20 @@ export const IConfigurationResolverService = createDecorator): IStringDictionary; - resolveAny(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary): T; + resolve(folder: IWorkspaceFolder, value: string): string; + resolve(folder: IWorkspaceFolder, value: string[]): string[]; + resolve(folder: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; + + /** + * Recursively resolves all variables in the given config and returns a copy of it with substituted values. + * Command variables are only substituted if a "commandValueMapping" dictionary is given and if it contains an entry for the command. + */ + resolveAny(folder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any; + + /** + * Recursively resolves all variables (including commands) in the given config and returns a copy of it with substituted values. + * If a "variables" dictionary (with names -> command ids) is given, + * command variables are first mapped through it before being resolved. + */ resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise; } diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts index 1948ae812de..e4e47dce829 100644 --- a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -7,27 +7,22 @@ import uri from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; -import * as objects from 'vs/base/common/objects'; import { Schemas } from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; import { toResource } from 'vs/workbench/common/editor'; import { IStringDictionary, size } from 'vs/base/common/collections'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isUndefinedOrNull } from 'vs/base/common/types'; -export class ConfigurationResolverService implements IConfigurationResolverService { - - _serviceBrand: any; - private resolver: VariableResolver; +export class ConfigurationResolverService extends AbstractVariableResolverService { constructor( envVariables: platform.IProcessEnvironment, @@ -37,7 +32,7 @@ export class ConfigurationResolverService implements IConfigurationResolverServi @ICommandService private commandService: ICommandService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService ) { - this.resolver = new VariableResolver({ + super({ getFolderUri: (folderName: string): uri => { const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); return folder ? folder.uri : undefined; @@ -84,21 +79,10 @@ export class ConfigurationResolverService implements IConfigurationResolverServi }, envVariables); } - public resolve(root: IWorkspaceFolder, value: string): string; - public resolve(root: IWorkspaceFolder, value: string[]): string[]; - public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; - public resolve(root: IWorkspaceFolder, value: any): any { - return this.resolver.resolveAny(root ? root.uri : undefined, value); - } - - public resolveAny(root: IWorkspaceFolder, value: any, commandValueMapping?: IStringDictionary): any { - return this.resolver.resolveAny(root ? root.uri : undefined, value, commandValueMapping); - } - public resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise { // then substitute remaining variables in VS Code core - config = this.substituteVariables(folder, config); + config = this.resolveAny(folder, config); // now evaluate command variables (which might have a UI) return this.executeCommandVariables(config, variables).then(commandValueMapping => { @@ -109,37 +93,18 @@ export class ConfigurationResolverService implements IConfigurationResolverServi // finally substitute evaluated command variables (if there are any) if (size(commandValueMapping) > 0) { - return this.substituteVariables(folder, config, commandValueMapping); + return this.resolveAny(folder, config, commandValueMapping); } else { return config; } }); } - private substituteVariables(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any { - - const result = objects.deepClone(config) as any; - - // hoist platform specific attributes to top level - if (platform.isWindows && result.windows) { - Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); - } else if (platform.isMacintosh && result.osx) { - Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); - } else if (platform.isLinux && result.linux) { - Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); - } - - // delete all platform specific sections - delete result.windows; - delete result.osx; - delete result.linux; - - // substitute all variables in string values - return this.resolveAny(workspaceFolder, result, commandValueMapping); - } - /** - * Finds and executes all command variables (see #6569) + * Finds and executes all command variables in the given configuration and returns their values as a dictionary. + * Please note: this method does not substitute the command variables (so the configuration is not modified). + * The returned dictionary can be passed to "resolvePlatform" for the substitution. + * See #6569. */ private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { diff --git a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts index 9c118dc6be8..4a85965f30f 100644 --- a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts @@ -5,14 +5,18 @@ import * as paths from 'vs/base/common/paths'; import * as types from 'vs/base/common/types'; +import * as objects from 'vs/base/common/objects'; import { IStringDictionary } from 'vs/base/common/collections'; import { relative } from 'path'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { localize } from 'vs/nls'; import uri from 'vs/base/common/uri'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { TPromise } from 'vs/base/common/winjs.base'; -export interface IVariableAccessor { +export interface IVariableResolveContext { getFolderUri(folderName: string): uri | undefined; getWorkspaceFolderCount(): number; getConfigurationValue(folderUri: uri, section: string): string | undefined; @@ -22,47 +26,78 @@ export interface IVariableAccessor { getLineNumber(): string; } -export class VariableResolver { +export class AbstractVariableResolverService implements IConfigurationResolverService { static VARIABLE_REGEXP = /\$\{(.*?)\}/g; - private envVariables: IProcessEnvironment; + _serviceBrand: any; constructor( - private accessor: IVariableAccessor, - envVariables: IProcessEnvironment + private _context: IVariableResolveContext, + private _envVariables: IProcessEnvironment = process.env ) { if (isWindows) { - this.envVariables = Object.create(null); - Object.keys(envVariables).forEach(key => { - this.envVariables[key.toLowerCase()] = envVariables[key]; + this._envVariables = Object.create(null); + Object.keys(_envVariables).forEach(key => { + this._envVariables[key.toLowerCase()] = _envVariables[key]; }); - } else { - this.envVariables = envVariables; } } - resolveAny(folderUri: uri, value: any, commandValueMapping?: IStringDictionary): any { + public resolve(root: IWorkspaceFolder, value: string): string; + public resolve(root: IWorkspaceFolder, value: string[]): string[]; + public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; + public resolve(root: IWorkspaceFolder, value: any): any { + return this.recursiveResolve(root ? root.uri : undefined, value); + } + + public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any { + + const result = objects.deepClone(config) as any; + + // hoist platform specific attributes to top level + if (isWindows && result.windows) { + Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); + } else if (isMacintosh && result.osx) { + Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); + } else if (isLinux && result.linux) { + Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); + } + + // delete all platform specific sections + delete result.windows; + delete result.osx; + delete result.linux; + + // substitute all variables recursively in string values + return this.recursiveResolve(workspaceFolder ? workspaceFolder.uri : undefined, result, commandValueMapping); + } + + public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise { + throw new Error('resolveWithCommands not implemented.'); + } + + private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary): any { if (types.isString(value)) { - return this.resolve(folderUri, value, commandValueMapping); + return this.resolveString(folderUri, value, commandValueMapping); } else if (types.isArray(value)) { - return value.map(s => this.resolveAny(folderUri, s, commandValueMapping)); + return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping)); } else if (types.isObject(value)) { let result: IStringDictionary | string[]> = Object.create(null); Object.keys(value).forEach(key => { - const resolvedKey = this.resolve(folderUri, key, commandValueMapping); - result[resolvedKey] = this.resolveAny(folderUri, value[key], commandValueMapping); + const resolvedKey = this.resolveString(folderUri, key, commandValueMapping); + result[resolvedKey] = this.recursiveResolve(folderUri, value[key], commandValueMapping); }); return result; } return value; } - resolve(folderUri: uri, value: string, commandValueMapping: IStringDictionary): string { + private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary): string { - const filePath = this.accessor.getFilePath(); + const filePath = this._context.getFilePath(); - return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => { + return value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { let argument: string; const parts = variable.split(':'); @@ -78,7 +113,7 @@ export class VariableResolver { if (isWindows) { argument = argument.toLowerCase(); } - const env = this.envVariables[argument]; + const env = this._envVariables[argument]; if (types.isString(env)) { return env; } @@ -89,7 +124,7 @@ export class VariableResolver { case 'config': if (argument) { - const config = this.accessor.getConfigurationValue(folderUri, argument); + const config = this._context.getConfigurationValue(folderUri, argument); if (types.isUndefinedOrNull(config)) { throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument)); } @@ -120,7 +155,7 @@ export class VariableResolver { case 'workspaceFolderBasename': case 'relativeFile': if (argument) { - const folder = this.accessor.getFolderUri(argument); + const folder = this._context.getFolderUri(argument); if (folder) { folderUri = folder; } else { @@ -128,7 +163,7 @@ export class VariableResolver { } } if (!folderUri) { - if (this.accessor.getWorkspaceFolderCount() > 1) { + if (this._context.getWorkspaceFolderCount() > 1) { throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match)); } throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match)); @@ -167,14 +202,14 @@ export class VariableResolver { return paths.basename(folderUri.fsPath); case 'lineNumber': - const lineNumber = this.accessor.getLineNumber(); + const lineNumber = this._context.getLineNumber(); if (lineNumber) { return lineNumber; } throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match)); case 'selectedText': - const selectedText = this.accessor.getSelectedText(); + const selectedText = this._context.getSelectedText(); if (selectedText) { return selectedText; } @@ -203,7 +238,7 @@ export class VariableResolver { return basename.slice(0, basename.length - paths.extname(basename).length); case 'execPath': - const ep = this.accessor.getExecPath(); + const ep = this._context.getExecPath(); if (ep) { return ep; }