diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 0e58b584418..88adaf3043c 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -408,6 +408,7 @@ "./vs/workbench/services/untitled/common/untitledEditorService.ts", "./vs/workbench/services/viewlet/browser/viewlet.ts", "./vs/workbench/services/workspace/common/workspaceEditing.ts", + "./vs/workbench/services/configurationResolver/common/variableResolver.ts", "./vs/workbench/services/workspace/node/workspaceEditingService.ts", "./vs/workbench/test/browser/actionRegistry.test.ts", "./vs/workbench/test/browser/part.test.ts", diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 394527812e6..aa58ea59d29 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -869,7 +869,7 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ constructor(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider) { super({ - getFolderUri: (folderName: string): URI => { + getFolderUri: (folderName: string): URI | undefined => { const found = folders.filter(f => f.name === folderName); if (found && found.length > 0) { return found[0].uri; @@ -879,7 +879,7 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ getWorkspaceFolderCount: (): number => { return folders.length; }, - getConfigurationValue: (folderUri: URI, section: string) => { + getConfigurationValue: (folderUri: URI, section: string): string | undefined => { return configurationService.getConfiguration(undefined, folderUri).get(section); }, getExecPath: (): string | undefined => { @@ -902,7 +902,7 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ } return undefined; }, - getLineNumber: (): string => { + getLineNumber: (): string | undefined => { const activeEditor = editorService.activeEditor(); if (activeEditor) { return String(activeEditor.selection.end.line + 1); diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 4201c733ddc..fbe48aa6c23 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -22,7 +22,7 @@ export interface IVariableResolveContext { getExecPath(): string | undefined; getFilePath(): string | undefined; getSelectedText(): string | undefined; - getLineNumber(): string; + getLineNumber(): string | undefined; } export class AbstractVariableResolverService implements IConfigurationResolverService { @@ -90,187 +90,181 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe throw new Error('resolveWithInteraction not implemented.'); } - private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): any { + private recursiveResolve(folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): any { if (types.isString(value)) { - const resolved = this.resolveString(folderUri, value, commandValueMapping); - if (resolvedVariables) { - resolvedVariables.set(resolved.variableName, resolved.resolvedValue); - } - return resolved.replaced; + return this.resolveString(folderUri, value, commandValueMapping, resolvedVariables); } else if (types.isArray(value)) { return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping, resolvedVariables)); } else if (types.isObject(value)) { let result: IStringDictionary | string[]> = Object.create(null); Object.keys(value).forEach(key => { - const resolvedKey = this.resolveString(folderUri, key, commandValueMapping); - if (resolvedVariables) { - resolvedVariables.set(resolvedKey.variableName, resolvedKey.resolvedValue); - } - result[resolvedKey.replaced] = this.recursiveResolve(folderUri, value[key], commandValueMapping, resolvedVariables); + const replaced = this.resolveString(folderUri, key, commandValueMapping, resolvedVariables); + result[replaced] = this.recursiveResolve(folderUri, value[key], commandValueMapping, resolvedVariables); }); return result; } return value; } - private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary): { replaced: string, variableName: string, resolvedValue: string } { + private resolveString(folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary | undefined, resolvedVariables?: Map): string { - const filePath = this._context.getFilePath(); - let variableName: string | undefined; - let resolvedValue: string | undefined; + // loop through all variables occurrences in 'value' const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { - variableName = variable; - let argument: string | undefined; - const parts = variable.split(':'); - if (parts && parts.length > 1) { - variable = parts[0]; - argument = parts[1]; + let resolvedValue = this.evaluateSingleVariable(match, variable, folderUri, commandValueMapping); + + if (resolvedVariables) { + resolvedVariables.set(variable, resolvedValue); } - switch (variable) { - - case 'env': - if (argument) { - if (isWindows) { - argument = argument.toLowerCase(); - } - const env = this._envVariables[argument]; - if (types.isString(env)) { - return resolvedValue = env; - } - // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 - return resolvedValue = ''; - } - throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match)); - - case 'config': - if (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)); - } - if (types.isObject(config)) { - throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument)); - } - return resolvedValue = config; - } - throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match)); - - case 'command': - return resolvedValue = this.resolveFromMap(match, argument, commandValueMapping, 'command'); - case 'input': - return resolvedValue = this.resolveFromMap(match, argument, commandValueMapping, 'input'); - - default: { - - // common error handling for all variables that require an open folder and accept a folder name argument - switch (variable) { - case 'workspaceRoot': - case 'workspaceFolder': - case 'workspaceRootFolderName': - case 'workspaceFolderBasename': - case 'relativeFile': - if (argument) { - const folder = this._context.getFolderUri(argument); - if (folder) { - folderUri = folder; - } else { - throw new Error(localize('canNotFindFolder', "'{0}' can not be resolved. No such folder '{1}'.", match, argument)); - } - } - if (!folderUri) { - 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)); - } - break; - default: - break; - } - - // common error handling for all variables that require an open file - switch (variable) { - case 'file': - case 'relativeFile': - case 'fileDirname': - case 'fileExtname': - case 'fileBasename': - case 'fileBasenameNoExtension': - if (!filePath) { - throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match)); - } - break; - default: - break; - } - - switch (variable) { - case 'workspaceRoot': - case 'workspaceFolder': - return resolvedValue = normalizeDriveLetter(folderUri.fsPath); - - case 'cwd': - return resolvedValue = (folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd()); - - case 'workspaceRootFolderName': - case 'workspaceFolderBasename': - return resolvedValue = paths.basename(folderUri.fsPath); - - case 'lineNumber': - const lineNumber = this._context.getLineNumber(); - if (lineNumber) { - return resolvedValue = 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._context.getSelectedText(); - if (selectedText) { - return resolvedValue = selectedText; - } - throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match)); - - case 'file': - return resolvedValue = filePath; - - case 'relativeFile': - if (folderUri) { - return resolvedValue = paths.normalize(paths.relative(folderUri.fsPath, filePath)); - } - return resolvedValue = filePath; - - case 'fileDirname': - return resolvedValue = paths.dirname(filePath); - - case 'fileExtname': - return resolvedValue = paths.extname(filePath); - - case 'fileBasename': - return resolvedValue = paths.basename(filePath); - - case 'fileBasenameNoExtension': - const basename = paths.basename(filePath); - return resolvedValue = (basename.slice(0, basename.length - paths.extname(basename).length)); - - case 'execPath': - const ep = this._context.getExecPath(); - if (ep) { - return resolvedValue = ep; - } - return resolvedValue = match; - - default: - return resolvedValue = match; - } - } - } + return resolvedValue; }); - return { replaced, variableName, resolvedValue }; + + return replaced; } - private resolveFromMap(match: string, argument: string, commandValueMapping: IStringDictionary, prefix: string): string { + private evaluateSingleVariable(match: string, variable: string, folderUri0: uri | undefined, commandValueMapping: IStringDictionary | undefined): string { + + // try to separate variable arguments from variable name + let argument: string | undefined; + const parts = variable.split(':'); + if (parts.length > 1) { + variable = parts[0]; + argument = parts[1]; + } + + // common error handling for all variables that require an open editor + const getFilePath = (): string => { + + const filePath = this._context.getFilePath(); + if (filePath) { + return filePath; + } + throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match)); + }; + + // common error handling for all variables that require an open folder and accept a folder name argument + const getFolderUri = (): uri => { + + if (argument) { + const folder = this._context.getFolderUri(argument); + if (folder) { + return folder; + } + throw new Error(localize('canNotFindFolder', "'{0}' can not be resolved. No such folder '{1}'.", match, argument)); + } + + if (folderUri0) { + return folderUri0; + } + + 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)); + }; + + + 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 name is given.", match)); + + case 'config': + if (argument) { + const config = this._context.getConfigurationValue(getFolderUri(), argument); + if (types.isUndefinedOrNull(config)) { + throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument)); + } + if (types.isObject(config)) { + throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument)); + } + return config; + } + throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match)); + + case 'command': + return this.resolveFromMap(match, argument, commandValueMapping, 'command'); + + case 'input': + return this.resolveFromMap(match, argument, commandValueMapping, 'input'); + + default: { + + switch (variable) { + case 'workspaceRoot': + case 'workspaceFolder': + return normalizeDriveLetter(getFolderUri().fsPath); + + case 'cwd': + return (folderUri0 ? normalizeDriveLetter(getFolderUri().fsPath) : process.cwd()); + + case 'workspaceRootFolderName': + case 'workspaceFolderBasename': + return paths.basename(getFolderUri().fsPath); + + case 'lineNumber': + 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._context.getSelectedText(); + if (selectedText) { + return selectedText; + } + throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match)); + + case 'file': + return getFilePath(); + + case 'relativeFile': + if (folderUri0) { + return paths.normalize(paths.relative(getFolderUri().fsPath, getFilePath())); + } + return getFilePath(); + + case 'fileDirname': + return paths.dirname(getFilePath()); + + case 'fileExtname': + return paths.extname(getFilePath()); + + case 'fileBasename': + return paths.basename(getFilePath()); + + case 'fileBasenameNoExtension': + const basename = paths.basename(getFilePath()); + return (basename.slice(0, basename.length - paths.extname(basename).length)); + + case 'execPath': + const ep = this._context.getExecPath(); + if (ep) { + return ep; + } + return match; + + default: + return match; + } + } + } + } + + private resolveFromMap(match: string, argument: string | undefined, commandValueMapping: IStringDictionary | undefined, prefix: string): string { if (argument && commandValueMapping) { const v = commandValueMapping[prefix + ':' + argument]; if (typeof v === 'string') {