diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index c43140980b6..893b8962517 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -16,6 +16,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC import severity from 'vs/base/common/severity'; import { AbstractDebugAdapter, convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/debugAdapter'; import * as paths from 'vs/base/common/paths'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @extHostNamedCustomer(MainContext.MainThreadDebugService) @@ -66,6 +67,10 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return da; } + substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { + return this._proxy.$substituteVariables(folder.uri, config); + } + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { return this._proxy.$runInTerminal(args, config); } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 4c5cce2c9db..a4a7b247fce 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -107,7 +107,7 @@ export function createApiFactory( const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(rpcProtocol, extHostHeapService, extHostLogService)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands)); rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace); - const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService)); + const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService, extHostDocumentsAndEditors, extHostConfiguration)); rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 6bd721a9ec9..7e1410d4de1 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -790,6 +790,7 @@ export interface ISourceMultiBreakpointDto { } export interface ExtHostDebugServiceShape { + $substituteVariables(folder: UriComponents | undefined, config: IConfig): TPromise; $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null): TPromise; $stopDASession(handle: number): TPromise; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 7e7a0fe405b..c8b29476bb2 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -4,6 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as paths from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; import { asWinJsPromise } from 'vs/base/common/async'; @@ -11,16 +14,20 @@ import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IMainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto } from 'vs/workbench/api/node/extHost.protocol'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; -import URI, { UriComponents } from 'vs/base/common/uri'; import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint } from 'vs/workbench/api/node/extHostTypes'; import { generateUuid } from 'vs/base/common/uuid'; import { DebugAdapter, convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/debugAdapter'; -import * as paths from 'vs/base/common/paths'; +import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; -import { IAdapterExecutable, ITerminalSettings, IDebuggerContribution } from 'vs/workbench/parts/debug/common/debug'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { IAdapterExecutable, ITerminalSettings, IDebuggerContribution, IConfig } from 'vs/workbench/parts/debug/common/debug'; import { getTerminalLauncher } 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 { IConfigurationResolverService } from '../../services/configurationResolver/common/configurationResolver'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { ExtHostConfiguration } from './extHostConfiguration'; export class ExtHostDebugService implements ExtHostDebugServiceShape { @@ -56,8 +63,15 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private _debugAdapters: Map; + private _variableResolver: IConfigurationResolverService; - constructor(mainContext: IMainContext, private _workspace: ExtHostWorkspace, private _extensionService: ExtHostExtensionService) { + + constructor(mainContext: IMainContext, + private _workspace: ExtHostWorkspace, + private _extensionService: ExtHostExtensionService, + private _editorsService: ExtHostDocumentsAndEditors, + private _configurationService: ExtHostConfiguration + ) { this._handleCounter = 0; this._handlers = new Map(); @@ -110,6 +124,14 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { return void 0; } + public $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): TPromise { + if (!this._variableResolver) { + this._variableResolver = new ExtHostVariableResolverService(this._workspace, this._editorsService, this._configurationService); + } + const folder = this.getFolder(folderUri); + return asWinJsPromise(token => DebugAdapter.substituteVariables(folder, config, this._variableResolver)); + } + public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null): TPromise { const mythis = this; @@ -503,3 +525,71 @@ export class ExtHostDebugConsole implements vscode.DebugConsole { this.append(value + '\n'); } } + +class ExtHostVariableResolverService implements IConfigurationResolverService { + + _serviceBrand: any; + _variableResolver: VariableResolver; + + constructor(workspace: ExtHostWorkspace, editors: ExtHostDocumentsAndEditors, configuration: ExtHostConfiguration) { + this._variableResolver = new VariableResolver({ + getFolderUri: (folderName: string): URI => { + const folders = workspace.getWorkspaceFolders(); + const found = folders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspace.getWorkspaceFolders().length; + }, + getConfigurationValue: (folderUri: URI, section: string) => { + return configuration.getConfiguration(undefined, folderUri).get(section); + }, + getEnvironmentService: (name: string): string => { + return undefined; + }, + getFilePath: (): string | undefined => { + const activeEditor = editors.activeEditor(); + if (activeEditor) { + const resource = activeEditor.document.uri; + if (resource.scheme === Schemas.file) { + return paths.normalize(resource.fsPath, true); + } + } + return undefined; + }, + getSelectedText: (): string | undefined => { + debugger; + const activeEditor = editors.activeEditor(); + if (activeEditor && !activeEditor.selection.isEmpty) { + return activeEditor.document.getText(activeEditor.selection); + } + return undefined; + }, + getLineNumber: (): string => { + const activeEditor = editors.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: any): any { + return this._variableResolver.resolveAny(root ? root.uri : undefined, value); + } + + resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string; }): TPromise { + throw new Error('Method not implemented.'); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 6477133f88a..566acd9d9af 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -360,23 +360,29 @@ export interface IGlobalConfig { } export interface IEnvConfig { - name?: string; - type: string; - request: string; internalConsoleOptions?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; preLaunchTask?: string; postDebugTask?: string; - __restart?: any; - __sessionId?: string; debugServer?: number; noDebug?: boolean; - port?: number; } export interface IConfig extends IEnvConfig { + + // fundamental attributes + type: string; + request: string; + name?: string; + + // platform specifics windows?: IEnvConfig; osx?: IEnvConfig; linux?: IEnvConfig; + + // internals + __sessionId?: string; + __restart?: any; + port?: number; // TODO } export interface ICompound { @@ -398,6 +404,7 @@ export interface IDebugAdapter extends IDisposable { export interface IDebugAdapterProvider extends ITerminalLauncher { createDebugAdapter(debugType: string, adapterInfo: IAdapterExecutable | null): IDebugAdapter; + substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise; } export interface IAdapterExecutable { @@ -497,6 +504,7 @@ export interface IConfigurationManager { registerDebugAdapterProvider(debugTypes: string[], debugAdapterLauncher: IDebugAdapterProvider): IDisposable; createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable | null): IDebugAdapter | undefined; + substituteVariables(debugType: string, folder: IWorkspaceFolder, config: IConfig): TPromise; runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; } @@ -540,12 +548,6 @@ export interface ILaunch { */ getConfigurationNames(includeCompounds?: boolean): string[]; - /** - * Returns the resolved configuration. - * Replaces os specific values, system variables, interactive variables. - */ - substituteVariables(config: IConfig): TPromise; - /** * Opens the launch.json file. Creates if it does not exist. */ diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 72681ca18a1..1428ee55ed1 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -8,7 +8,6 @@ import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import * as objects from 'vs/base/common/objects'; import uri from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; @@ -26,7 +25,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, IDebuggerContribution, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter, ITerminalSettings, ITerminalLauncher } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugConfigurationProvider, IDebuggerContribution, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter, ITerminalSettings, ITerminalLauncher } from 'vs/workbench/parts/debug/common/debug'; import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -242,7 +241,8 @@ export class ConfigurationManager implements IConfigurationManager { @IInstantiationService private instantiationService: IInstantiationService, @ICommandService private commandService: ICommandService, @IStorageService private storageService: IStorageService, - @ILifecycleService lifecycleService: ILifecycleService + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService ) { this.providers = []; this.debuggers = []; @@ -326,6 +326,14 @@ export class ConfigurationManager implements IConfigurationManager { return undefined; } + public substituteVariables(debugType: string, folder: IWorkspaceFolder, config: IConfig): TPromise { + let dap = this.getDebugAdapterProvider(debugType); + if (dap) { + return dap.substituteVariables(folder, config); + } + return TPromise.as(config); + } + public runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { let tl: ITerminalLauncher = this.getDebugAdapterProvider(debugType); @@ -355,7 +363,7 @@ export class ConfigurationManager implements IConfigurationManager { if (duplicate) { duplicate.merge(rawAdapter, extension.description); } else { - this.debuggers.push(new Debugger(this, rawAdapter, extension.description, this.configurationService, this.commandService)); + this.debuggers.push(new Debugger(this, rawAdapter, extension.description, this.configurationService, this.commandService, this.configurationResolverService)); } }); }); @@ -548,7 +556,6 @@ class Launch implements ILaunch { @IFileService private fileService: IFileService, @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, @IConfigurationService protected configurationService: IConfigurationService, - @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExtensionService private extensionService: IExtensionService ) { @@ -607,45 +614,6 @@ class Launch implements ILaunch { return config.configurations.filter(config => config && config.name === name).shift(); } - protected getWorkspaceForResolving(): IWorkspaceFolder { - if (this.workspace) { - return this.workspace; - } - - if (this.contextService.getWorkspace().folders.length === 1) { - return this.contextService.getWorkspace().folders[0]; - } - - return undefined; - } - - public substituteVariables(config: IConfig): TPromise { - const result = objects.deepClone(config) as IConfig; - // Set operating system specific properties #1873 - const setOSProperties = (flag: boolean, osConfig: IEnvConfig) => { - if (flag && osConfig) { - Object.keys(osConfig).forEach(key => { - result[key] = osConfig[key]; - }); - } - }; - setOSProperties(isWindows, result.windows); - setOSProperties(isMacintosh, result.osx); - setOSProperties(isLinux, result.linux); - - // massage configuration attributes - append workspace path to relatvie paths, substitute variables in paths. - try { - Object.keys(result).forEach(key => { - result[key] = this.configurationResolverService.resolveAny(this.getWorkspaceForResolving(), result[key]); - }); - } catch (e) { - return TPromise.wrapError(e); - } - - const adapter = this.configurationManager.getDebugger(result.type); - return this.configurationResolverService.resolveInteractiveVariables(result, adapter ? adapter.variables : null); - } - public openConfigFile(sideBySide: boolean, type?: string): TPromise { return this.extensionService.activateByEvent('onDebugInitialConfigurations').then(() => this.extensionService.activateByEvent('onDebug').then(() => { const resource = this.uri; @@ -715,7 +683,7 @@ class WorkspaceLaunch extends Launch implements ILaunch { @IWorkspaceContextService contextService: IWorkspaceContextService, @IExtensionService extensionService: IExtensionService ) { - super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, contextService, extensionService); + super(configurationManager, undefined, fileService, editorService, configurationService, contextService, extensionService); } get uri(): uri { @@ -747,7 +715,7 @@ class UserLaunch extends Launch implements ILaunch { @IWorkspaceContextService contextService: IWorkspaceContextService, @IExtensionService extensionService: IExtensionService ) { - super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, contextService, extensionService); + super(configurationManager, undefined, fileService, editorService, configurationService, contextService, extensionService); } get uri(): uri { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 0987dbaca10..afa634c5d1c 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -784,9 +784,32 @@ export class DebugService implements debug.IDebugService { }); } + private substituteVariables(launch: debug.ILaunch, config: debug.IConfig): TPromise { + const dbg = this.configurationManager.getDebugger(config.type); + if (dbg) { + let folder: IWorkspaceFolder = undefined; + if (launch.workspace) { + folder = launch.workspace; + } else { + const folders = this.contextService.getWorkspace().folders; + if (folders.length === 1) { + folder = folders[0]; + } + } + return dbg.substituteVariables(folder, config).then(config => { + return config; + }, (err: Error) => { + this.showError(err.message); + return undefined; // bail out + }); + } + return TPromise.as(config); + } + private createProcess(launch: debug.ILaunch, config: debug.IConfig, sessionId: string): TPromise { return this.textFileService.saveAll().then(() => - (launch ? launch.substituteVariables(config).then(config => config, (err: Error) => this.showError(err.message)) : TPromise.as(config)).then(resolvedConfig => { + this.substituteVariables(launch, config).then(resolvedConfig => { + if (!resolvedConfig) { // User canceled resolving of interactive variables, silently return return undefined; diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index f09c6fadc0e..c74bd1c9e4d 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -17,7 +17,9 @@ 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 } from 'vs/workbench/parts/debug/common/debug'; +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'; /** * Abstract implementation of the low level API for a debug adapter. @@ -406,6 +408,28 @@ export class DebugAdapter extends StreamDebugAdapter { }; } } + + static substituteVariables(workspaceFolder: IWorkspaceFolder, config: IConfig, resolverService: IConfigurationResolverService): 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); + } } // path hooks helpers diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index b3e1907e2ce..e6e1ffed7ab 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -15,6 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { DebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; export class Debugger { @@ -22,7 +23,8 @@ export class Debugger { constructor(private configurationManager: IConfigurationManager, private debuggerContribution: IDebuggerContribution, public extensionDescription: IExtensionDescription, @IConfigurationService private configurationService: IConfigurationService, - @ICommandService private commandService: ICommandService + @ICommandService private commandService: ICommandService, + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, ) { this._mergedExtensionDescriptions = [extensionDescription]; } @@ -59,6 +61,26 @@ export class Debugger { }); } + public substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { + + let configP: TPromise; + const debugConfigs = this.configurationService.getValue('debug'); + if (debugConfigs.extensionHostDebugAdapter) { + configP = this.configurationManager.substituteVariables(this.type, folder, config); + } else { + try { + configP = TPromise.as(DebugAdapter.substituteVariables(folder, config, this.configurationResolverService)); + } catch (e) { + return TPromise.wrapError(e); + } + } + + return configP.then(result => { + // substitute 'command' variables (including interactive) + return this.configurationResolverService.resolveInteractiveVariables(result, this.variables); + }); + } + public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise { const debugConfigs = this.configurationService.getValue('debug'); const config = this.configurationService.getValue('terminal'); diff --git a/src/vs/workbench/parts/debug/test/node/debugger.test.ts b/src/vs/workbench/parts/debug/test/node/debugger.test.ts index 9e57914a9a9..0e7dadf2ea2 100644 --- a/src/vs/workbench/parts/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/parts/debug/test/node/debugger.test.ts @@ -120,7 +120,7 @@ suite('Debug - Debugger', () => { }; setup(() => { - _debugger = new Debugger(configurationManager, debuggerContribution, extensionDescriptor0, new TestConfigurationService(), null); + _debugger = new Debugger(configurationManager, debuggerContribution, extensionDescriptor0, new TestConfigurationService(), null, null); }); teardown(() => { diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts index 833a69b8929..26c6ee9cf95 100644 --- a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import uri from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; -import * as types from 'vs/base/common/types'; +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 } from 'vs/base/common/collections'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -14,210 +16,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { toResource } from 'vs/workbench/common/editor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { relative } from 'path'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { Schemas } from 'vs/base/common/network'; -import { localize } from 'vs/nls'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -class VariableResolver { - static VARIABLE_REGEXP = /\$\{(.*?)\}/g; - private envVariables: IProcessEnvironment; - - constructor( - envVariables: IProcessEnvironment, - private configurationService: IConfigurationService, - private editorService: IWorkbenchEditorService, - private environmentService: IEnvironmentService, - private workspaceContextService: IWorkspaceContextService - ) { - if (isWindows) { - this.envVariables = Object.create(null); - Object.keys(envVariables).forEach(key => { - this.envVariables[key.toLowerCase()] = envVariables[key]; - }); - } else { - this.envVariables = envVariables; - } - } - - resolve(context: IWorkspaceFolder, value: string): string { - const filePath = this.getFilePath(); - return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => { - const parts = variable.split(':'); - let sufix: string; - if (parts && parts.length > 1) { - variable = parts[0]; - sufix = parts[1]; - } - - switch (variable) { - case 'env': { - if (sufix) { - if (isWindows) { - sufix = sufix.toLowerCase(); - } - - const env = this.envVariables[sufix]; - if (types.isString(env)) { - return env; - } - - // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 - return ''; - } - } - case 'config': { - if (sufix) { - const config = this.configurationService.getValue(sufix, context ? { resource: context.uri } : undefined); - if (!types.isUndefinedOrNull(config) && !types.isObject(config)) { - return config; - } - } - } - default: { - if (sufix) { - const folder = this.workspaceContextService.getWorkspace().folders.filter(f => f.name === sufix).pop(); - if (folder) { - context = folder; - } - } - - switch (variable) { - case 'workspaceRoot': - case 'workspaceFolder': { - if (context) { - return normalizeDriveLetter(context.uri.fsPath); - } - if (this.workspaceContextService.getWorkspace().folders.length > 1) { - throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'${workspaceFolder}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.")); - } - - throw new Error(localize('canNotResolveWorkspaceFolder', "'${workspaceFolder}' can not be resolved. Please open a folder.")); - } case 'cwd': - return context ? normalizeDriveLetter(context.uri.fsPath) : process.cwd(); - case 'workspaceRootFolderName': - case 'workspaceFolderBasename': { - if (context) { - return paths.basename(context.uri.fsPath); - } - if (this.workspaceContextService.getWorkspace().folders.length > 1) { - throw new Error(localize('canNotResolveFolderBasenameMultiRoot', "'${workspaceFolderBasename}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.")); - } - - throw new Error(localize('canNotResolveFolderBasename', "'${workspaceFolderBasename}' can not be resolved. Please open a folder.")); - } case 'lineNumber': { - const lineNumber = this.getLineNumber(); - if (lineNumber) { - return lineNumber; - } - - throw new Error(localize('canNotResolveLineNumber', "'${lineNumber}' can not be resolved. Please open an editor.")); - } case 'selectedText': { - const selectedText = this.getSelectedText(); - if (selectedText) { - return selectedText; - } - - throw new Error(localize('canNotResolveSelectedText', "'${selectedText}' can not be resolved. Please open an editor.")); - } case 'file': { - if (filePath) { - return filePath; - } - - throw new Error(localize('canNotResolveFile', "'${file}' can not be resolved. Please open an editor.")); - } case 'relativeFile': { - if (context && filePath) { - return paths.normalize(relative(context.uri.fsPath, filePath)); - } - if (filePath) { - return filePath; - } - - throw new Error(localize('canNotResolveRelativeFile', "'${relativeFile}' can not be resolved. Please open an editor.")); - } case 'fileDirname': { - if (filePath) { - return paths.dirname(filePath); - } - - throw new Error(localize('canNotResolveFileDirname', "'${fileDirname}' can not be resolved. Please open an editor.")); - } case 'fileExtname': { - if (filePath) { - return paths.extname(filePath); - } - - throw new Error(localize('canNotResolveFileExtname', "'${fileExtname}' can not be resolved. Please open an editor.")); - } case 'fileBasename': { - if (filePath) { - return paths.basename(filePath); - } - - throw new Error(localize('canNotResolveFileBasename', "'${fileBasename}' can not be resolved. Please open an editor.")); - } case 'fileBasenameNoExtension': { - if (filePath) { - const basename = paths.basename(filePath); - return basename.slice(0, basename.length - paths.extname(basename).length); - } - - throw new Error(localize('canNotResolveFileBasenameNoExtension', "'${fileBasenameNoExtension}' can not be resolved. Please open an editor.")); - } - case 'execPath': - return this.environmentService.execPath; - - default: - return match; - } - } - } - }); - } - - private getSelectedText(): string { - const activeEditor = this.editorService.getActiveEditor(); - if (activeEditor) { - const editorControl = (activeEditor.getControl()); - if (editorControl) { - const editorModel = editorControl.getModel(); - const editorSelection = editorControl.getSelection(); - if (editorModel && editorSelection) { - return editorModel.getValueInRange(editorSelection); - } - } - } - - return undefined; - } - - private getFilePath(): string { - let input = this.editorService.getActiveEditorInput(); - if (input instanceof DiffEditorInput) { - input = input.modifiedInput; - } - - const fileResource = toResource(input, { filter: Schemas.file }); - if (!fileResource) { - return undefined; - } - - return paths.normalize(fileResource.fsPath, true); - } - - private getLineNumber(): string { - const activeEditor = this.editorService.getActiveEditor(); - if (activeEditor) { - const editorControl = (activeEditor.getControl()); - if (editorControl) { - const lineNumber = editorControl.getSelection().positionLineNumber; - return String(lineNumber); - } - } - - return undefined; - } -} export class ConfigurationResolverService implements IConfigurationResolverService { _serviceBrand: any; @@ -231,42 +34,68 @@ export class ConfigurationResolverService implements IConfigurationResolverServi @ICommandService private commandService: ICommandService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService ) { - this.resolver = new VariableResolver(envVariables, configurationService, editorService, environmentService, workspaceContextService); + this.resolver = new VariableResolver({ + getFolderUri: (folderName: string): uri => { + const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); + return folder ? folder.uri : undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceContextService.getWorkspace().folders.length; + }, + getConfigurationValue: (folderUri: uri, suffix: string) => { + return configurationService.getValue(suffix, folderUri ? { resource: folderUri } : undefined); + }, + getEnvironmentService: (name: string) => { + return environmentService[name]; + }, + getFilePath: (): string | undefined => { + let input = editorService.getActiveEditorInput(); + if (input instanceof DiffEditorInput) { + input = input.modifiedInput; + } + const fileResource = toResource(input, { filter: Schemas.file }); + if (!fileResource) { + return undefined; + } + return paths.normalize(fileResource.fsPath, true); + }, + getSelectedText: (): string | undefined => { + const activeEditor = editorService.getActiveEditor(); + if (activeEditor) { + const editorControl = (activeEditor.getControl()); + if (editorControl) { + const editorModel = editorControl.getModel(); + const editorSelection = editorControl.getSelection(); + if (editorModel && editorSelection) { + return editorModel.getValueInRange(editorSelection); + } + } + } + return undefined; + }, + getLineNumber: (): string => { + const activeEditor = editorService.getActiveEditor(); + if (activeEditor) { + const editorControl = (activeEditor.getControl()); + if (editorControl) { + const lineNumber = editorControl.getSelection().positionLineNumber; + return String(lineNumber); + } + } + return undefined; + } + }, 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 { - if (types.isString(value)) { - return this.resolver.resolve(root, value); - } else if (types.isArray(value)) { - return value.map(s => this.resolver.resolve(root, s)); - } else if (types.isObject(value)) { - let result: IStringDictionary | string[]> = Object.create(null); - Object.keys(value).forEach(key => { - result[key] = this.resolve(root, value[key]); - }); - - return result; - } - return value; + return this.resolver.resolveAny(root ? root.uri : undefined, value); } public resolveAny(root: IWorkspaceFolder, value: any): any { - if (types.isString(value)) { - return this.resolver.resolve(root, value); - } else if (types.isArray(value)) { - return value.map(s => this.resolveAny(root, s)); - } else if (types.isObject(value)) { - let result: IStringDictionary | string[]> = Object.create(null); - Object.keys(value).forEach(key => { - result[key] = this.resolveAny(root, value[key]); - }); - - return result; - } - return value; + return this.resolver.resolveAny(root ? root.uri : undefined, value); } /** diff --git a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts new file mode 100644 index 00000000000..edc0d4c0752 --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as paths from 'vs/base/common/paths'; +import * as types from 'vs/base/common/types'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { relative } from 'path'; +import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { localize } from 'vs/nls'; +import uri from 'vs/base/common/uri'; + + +export interface IVariableAccessor { + getFolderUri(folderName: string): uri | undefined; + getWorkspaceFolderCount(): number; + getConfigurationValue(folderUri: uri, section: string): string | undefined; + getEnvironmentService(name: string): string | undefined; + getFilePath(): string | undefined; + getSelectedText(): string | undefined; + getLineNumber(): string; +} + +export class VariableResolver { + + static VARIABLE_REGEXP = /\$\{(.*?)\}/g; + + private envVariables: IProcessEnvironment; + + constructor( + private accessor: IVariableAccessor, + envVariables: IProcessEnvironment + ) { + if (isWindows) { + 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): any { + if (types.isString(value)) { + return this.resolve(folderUri, value); + } else if (types.isArray(value)) { + return value.map(s => this.resolveAny(folderUri, s)); + } else if (types.isObject(value)) { + let result: IStringDictionary | string[]> = Object.create(null); + Object.keys(value).forEach(key => { + result[key] = this.resolveAny(folderUri, value[key]); + }); + return result; + } + return value; + } + + resolve(folderUri: uri, value: string): string { + + const filePath = this.accessor.getFilePath(); + + return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => { + + let argument: string; + const parts = variable.split(':'); + if (parts && parts.length > 1) { + variable = parts[0]; + argument = parts[1]; + } + + switch (variable) { + case 'env': + if (argument) { + if (isWindows) { + argument = argument.toLowerCase(); + } + const env = this.envVariables[argument]; + if (types.isString(env)) { + return env; + } + // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 + return ''; + } + throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable is given.", match)); + + case 'config': + if (argument) { + const config = this.accessor.getConfigurationValue(folderUri, argument); + if (!types.isUndefinedOrNull(config) && !types.isObject(config)) { + return config; + } + throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument)); + } + throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match)); + + default: { + + if (argument) { + const folder = this.accessor.getFolderUri(argument); + if (folder) { + folderUri = folder; + } + } + + switch (variable) { + case 'workspaceRoot': + case 'workspaceFolder': + if (folderUri) { + return normalizeDriveLetter(folderUri.fsPath); + } + if (this.accessor.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)); + + case 'cwd': + return folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd(); + + case 'workspaceRootFolderName': + case 'workspaceFolderBasename': + if (folderUri) { + return paths.basename(folderUri.fsPath); + } + if (this.accessor.getWorkspaceFolderCount() > 1) { + throw new Error(localize('canNotResolveFolderBasenameMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match)); + } + throw new Error(localize('canNotResolveFolderBasename', "'{0}' can not be resolved. Please open a folder.", match)); + + case 'lineNumber': + const lineNumber = this.accessor.getLineNumber(); + if (lineNumber) { + return lineNumber; + } + throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'selectedText': + const selectedText = this.accessor.getSelectedText(); + if (selectedText) { + return selectedText; + } + throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Please open an editor and select some text.", match)); + + case 'file': + if (filePath) { + return filePath; + } + throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'relativeFile': + if (folderUri && filePath) { + return paths.normalize(relative(folderUri.fsPath, filePath)); + } + if (filePath) { + return filePath; + } + throw new Error(localize('canNotResolveRelativeFile', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'fileDirname': + if (filePath) { + return paths.dirname(filePath); + } + throw new Error(localize('canNotResolveFileDirname', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'fileExtname': + if (filePath) { + return paths.extname(filePath); + } + throw new Error(localize('canNotResolveFileExtname', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'fileBasename': + if (filePath) { + return paths.basename(filePath); + } + throw new Error(localize('canNotResolveFileBasename', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'fileBasenameNoExtension': + if (filePath) { + const basename = paths.basename(filePath); + return basename.slice(0, basename.length - paths.extname(basename).length); + } + throw new Error(localize('canNotResolveFileBasenameNoExtension', "'{0}' can not be resolved. Please open an editor.", match)); + + case 'execPath': + return this.accessor.getEnvironmentService('execPath'); + + default: + return match; + } + } + } + }); + } +} diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index fe2ba2d40dd..5b514fb535b 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -196,18 +196,6 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); - test('configuration should not evaluate Javascript', () => { - let configurationService: IConfigurationService; - configurationService = new MockConfigurationService({ - editor: { - abc: 'foo' - } - }); - - let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor[\'abc\'.substr(0)]} xyz'), 'abc ${config:editor[\'abc\'.substr(0)]} xyz'); - }); - test('uses original variable as fallback', () => { let configurationService: IConfigurationService; configurationService = new MockConfigurationService({ @@ -215,10 +203,8 @@ suite('Configuration Resolver Service', () => { }); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); - assert.strictEqual(service.resolve(workspace, 'abc ${invalidVariable} xyz'), 'abc ${invalidVariable} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${env:invalidVariable} xyz'), 'abc xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.abc.def} xyz'), 'abc ${config:editor.abc.def} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:panel.abc} xyz'), 'abc ${config:panel.abc} xyz'); + assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); + assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); test('configuration variables with invalid accessor', () => { @@ -230,9 +216,14 @@ suite('Configuration Resolver Service', () => { }); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); - assert.strictEqual(service.resolve(workspace, 'abc ${config:} xyz'), 'abc ${config:} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz'), 'abc ${config:editor..fontFamily} xyz'); - assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.none.none2} xyz'), 'abc ${config:editor.none.none2} xyz'); + + assert.throws(() => service.resolve(workspace, 'abc ${env} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:editor} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz')); + assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz')); }); test('interactive variable simple', () => {