diff --git a/README.md b/README.md index 670195c3c92..b0a86b728ae 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ a code editor with what developers need for their core edit-build-debug cycle. Code provides comprehensive editing and debugging support, an extensibility model, and lightweight integration with existing tools. -VS Code is updated monthly with new features and bug fixes. You can download it for Windows, macOS, and Linux on [VS Code's website](https://code.visualstudio.com/Download). To get the latest releases everyday, you can install the [Insiders version of VS Code](https://code.visualstudio.com/insiders). This builds from the master branch and is updated at least daily. +VS Code is updated monthly with new features and bug fixes. You can download it for Windows, macOS, and Linux on [VS Code's website](https://code.visualstudio.com/Download). To get the latest releases every day, you can install the [Insiders version of VS Code](https://code.visualstudio.com/insiders). This builds from the master branch and is updated at least daily.

VS Code in action diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index bfa2a0bd8a3..f1807d26252 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -43,7 +43,7 @@ const nodeModules = ['electron', 'original-fs'] // Build const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.17.4' }, + { name: 'ms-vscode.node-debug', version: '1.17.5' }, { name: 'ms-vscode.node-debug2', version: '1.17.1' } ]; diff --git a/extensions/coffeescript/test/colorize-results/test-regex_coffee.json b/extensions/coffeescript/test/colorize-results/test-regex_coffee.json index 909a6112f35..445fab1f1c2 100644 --- a/extensions/coffeescript/test/colorize-results/test-regex_coffee.json +++ b/extensions/coffeescript/test/colorize-results/test-regex_coffee.json @@ -69,8 +69,8 @@ "c": "(", "t": "source.coffee string.regexp.coffee meta.group.regexp punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.regexp: #D7BA7D", - "light_plus": "punctuation.definition.group.regexp: #FF0000", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -80,8 +80,8 @@ "c": "\\d", "t": "source.coffee string.regexp.coffee meta.group.regexp constant.character.character-class.regexp", "r": { - "dark_plus": "constant.character: #569CD6", - "light_plus": "constant.character: #0000FF", + "dark_plus": "constant.character.character-class.regexp: #D16969", + "light_plus": "constant.character.character-class.regexp: #811F3F", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -91,8 +91,8 @@ "c": "+", "t": "source.coffee string.regexp.coffee meta.group.regexp keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -102,8 +102,8 @@ "c": ")", "t": "source.coffee string.regexp.coffee meta.group.regexp punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.regexp: #D7BA7D", - "light_plus": "punctuation.definition.group.regexp: #FF0000", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" diff --git a/extensions/coffeescript/test/colorize-results/test_coffee.json b/extensions/coffeescript/test/colorize-results/test_coffee.json index 4320ae579e6..a36d10633bf 100644 --- a/extensions/coffeescript/test/colorize-results/test_coffee.json +++ b/extensions/coffeescript/test/colorize-results/test_coffee.json @@ -1378,8 +1378,8 @@ "c": "(", "t": "source.coffee string.regexp.coffee meta.group.regexp punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.regexp: #D7BA7D", - "light_plus": "punctuation.definition.group.regexp: #FF0000", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -1389,8 +1389,8 @@ "c": "\\d", "t": "source.coffee string.regexp.coffee meta.group.regexp constant.character.character-class.regexp", "r": { - "dark_plus": "constant.character: #569CD6", - "light_plus": "constant.character: #0000FF", + "dark_plus": "constant.character.character-class.regexp: #D16969", + "light_plus": "constant.character.character-class.regexp: #811F3F", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -1400,8 +1400,8 @@ "c": "+", "t": "source.coffee string.regexp.coffee meta.group.regexp keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -1411,8 +1411,8 @@ "c": ")", "t": "source.coffee string.regexp.coffee meta.group.regexp punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.regexp: #D7BA7D", - "light_plus": "punctuation.definition.group.regexp: #FF0000", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -1466,8 +1466,8 @@ "c": "(", "t": "source.coffee string.regexp.coffee meta.group.regexp punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.regexp: #D7BA7D", - "light_plus": "punctuation.definition.group.regexp: #FF0000", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -1477,8 +1477,8 @@ "c": "\\w", "t": "source.coffee string.regexp.coffee meta.group.regexp constant.character.character-class.regexp", "r": { - "dark_plus": "constant.character: #569CD6", - "light_plus": "constant.character: #0000FF", + "dark_plus": "constant.character.character-class.regexp: #D16969", + "light_plus": "constant.character.character-class.regexp: #811F3F", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -1488,8 +1488,8 @@ "c": "*", "t": "source.coffee string.regexp.coffee meta.group.regexp keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -1499,8 +1499,8 @@ "c": ")", "t": "source.coffee string.regexp.coffee meta.group.regexp punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.regexp: #D7BA7D", - "light_plus": "punctuation.definition.group.regexp: #FF0000", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -1554,8 +1554,8 @@ "c": "$", "t": "source.coffee string.regexp.coffee keyword.control.anchor.regexp", "r": { - "dark_plus": "keyword.control.anchor.regexp: #C586C0", - "light_plus": "keyword.control.anchor.regexp: #09885A", + "dark_plus": "keyword.control.anchor.regexp: #DCDCAA", + "light_plus": "keyword.control.anchor.regexp: #FF0000", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0" diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index 597cc043d4e..e241ced459a 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -27,10 +27,6 @@ function getIPCHandlePath(nonce: string): string { return `\\\\.\\pipe\\vscode-git-askpass-${nonce}-sock`; } - if (process.env['XDG_RUNTIME_DIR']) { - return path.join(process.env['XDG_RUNTIME_DIR'], `vscode-git-askpass-${nonce}.sock`); - } - return path.join(os.tmpdir(), `vscode-git-askpass-${nonce}.sock`); } diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 0758d440b08..4c028b73312 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -31,6 +31,7 @@ "title": "Grunt", "properties": { "grunt.autoDetect": { + "scope": "resource", "type": "string", "enum": [ "off", diff --git a/extensions/grunt/src/main.ts b/extensions/grunt/src/main.ts index 554bb4963e6..a5e74585e8e 100644 --- a/extensions/grunt/src/main.ts +++ b/extensions/grunt/src/main.ts @@ -13,49 +13,6 @@ import * as nls from 'vscode-nls'; const localize = nls.config(process.env.VSCODE_NLS_CONFIG)(); type AutoDetect = 'on' | 'off'; -let taskProvider: vscode.Disposable | undefined; - -export function activate(_context: vscode.ExtensionContext): void { - let workspaceRoot = vscode.workspace.rootPath; - if (!workspaceRoot) { - return; - } - let pattern = path.join(workspaceRoot, '[Gg]runtfile.js'); - let detectorPromise: Thenable | undefined = undefined; - let fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); - fileWatcher.onDidChange(() => detectorPromise = undefined); - fileWatcher.onDidCreate(() => detectorPromise = undefined); - fileWatcher.onDidDelete(() => detectorPromise = undefined); - - function onConfigurationChanged() { - let autoDetect = vscode.workspace.getConfiguration('grunt').get('autoDetect'); - if (taskProvider && autoDetect === 'off') { - detectorPromise = undefined; - taskProvider.dispose(); - taskProvider = undefined; - } else if (!taskProvider && autoDetect === 'on') { - taskProvider = vscode.workspace.registerTaskProvider('grunt', { - provideTasks: () => { - if (!detectorPromise) { - detectorPromise = getGruntTasks(); - } - return detectorPromise; - }, - resolveTask(_task: vscode.Task): vscode.Task | undefined { - return undefined; - } - }); - } - } - vscode.workspace.onDidChangeConfiguration(onConfigurationChanged); - onConfigurationChanged(); -} - -export function deactivate(): void { - if (taskProvider) { - taskProvider.dispose(); - } -} function exists(file: string): Promise { return new Promise((resolve, _reject) => { @@ -76,19 +33,6 @@ function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: strin }); } -let _channel: vscode.OutputChannel; -function getOutputChannel(): vscode.OutputChannel { - if (!_channel) { - _channel = vscode.window.createOutputChannel('Grunt Auto Detection'); - } - return _channel; -} - -interface GruntTaskDefinition extends vscode.TaskDefinition { - task: string; - file?: string; -} - const buildNames: string[] = ['build', 'compile', 'watch']; function isBuildTask(name: string): boolean { for (let buildName of buildNames) { @@ -109,97 +53,287 @@ function isTestTask(name: string): boolean { return false; } -async function getGruntTasks(): Promise { - let workspaceRoot = vscode.workspace.rootPath; - let emptyTasks: vscode.Task[] = []; - if (!workspaceRoot) { - return emptyTasks; +let _channel: vscode.OutputChannel; +function getOutputChannel(): vscode.OutputChannel { + if (!_channel) { + _channel = vscode.window.createOutputChannel('Gulp Auto Detection'); } - if (!await exists(path.join(workspaceRoot, 'gruntfile.js')) && !await exists(path.join(workspaceRoot, 'Gruntfile.js'))) { - return emptyTasks; + return _channel; +} + +interface GruntTaskDefinition extends vscode.TaskDefinition { + task: string; + file?: string; +} + +class FolderDetector { + + private fileWatcher: vscode.FileSystemWatcher; + private promise: Thenable | undefined; + + constructor(private _workspaceFolder: vscode.WorkspaceFolder) { } - let command: string; - let platform = process.platform; - if (platform === 'win32' && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'grunt.cmd'))) { - command = path.join('.', 'node_modules', '.bin', 'grunt.cmd'); - } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'grunt'))) { - command = path.join('.', 'node_modules', '.bin', 'grunt'); - } else { - command = 'grunt'; + public get workspaceFolder(): vscode.WorkspaceFolder { + return this._workspaceFolder; } - let commandLine = `${command} --help --no-color`; - try { - let { stdout, stderr } = await exec(commandLine, { cwd: workspaceRoot }); - if (stderr) { - getOutputChannel().appendLine(stderr); - getOutputChannel().show(true); + public isEnabled(): boolean { + return vscode.workspace.getConfiguration('grunt', this._workspaceFolder.uri).get('autoDetect') === 'on'; + } + + public start(): void { + let pattern = path.join(this._workspaceFolder.uri.fsPath, '[Gg]runtfile.js'); + this.fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); + this.fileWatcher.onDidChange(() => this.promise = undefined); + this.fileWatcher.onDidCreate(() => this.promise = undefined); + this.fileWatcher.onDidDelete(() => this.promise = undefined); + } + + public async getTasks(): Promise { + if (!this.promise) { + this.promise = this.computeTasks(); } - let result: vscode.Task[] = []; - if (stdout) { - // grunt lists tasks as follows (description is wrapped into a new line if too long): - // ... - // Available tasks - // uglify Minify files with UglifyJS. * - // jshint Validate files with JSHint. * - // test Alias for "jshint", "qunit" tasks. - // default Alias for "jshint", "qunit", "concat", "uglify" tasks. - // long Alias for "eslint", "qunit", "browserify", "sass", - // "autoprefixer", "uglify", tasks. - // - // Tasks run in the order specified + return this.promise; + } - let lines = stdout.split(/\r{0,1}\n/); - let tasksStart = false; - let tasksEnd = false; - for (let line of lines) { - if (line.length === 0) { - continue; - } - if (!tasksStart && !tasksEnd) { - if (line.indexOf('Available tasks') === 0) { - tasksStart = true; + private async computeTasks(): Promise { + let rootPath = this._workspaceFolder.uri.scheme === 'file' ? this._workspaceFolder.uri.fsPath : undefined; + let emptyTasks: vscode.Task[] = []; + if (!rootPath) { + return emptyTasks; + } + if (!await exists(path.join(rootPath, 'gruntfile.js')) && !await exists(path.join(rootPath, 'Gruntfile.js'))) { + return emptyTasks; + } + + let command: string; + let platform = process.platform; + if (platform === 'win32' && await exists(path.join(rootPath!, 'node_modules', '.bin', 'grunt.cmd'))) { + command = path.join('.', 'node_modules', '.bin', 'grunt.cmd'); + } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(rootPath!, 'node_modules', '.bin', 'grunt'))) { + command = path.join('.', 'node_modules', '.bin', 'grunt'); + } else { + command = 'grunt'; + } + + let commandLine = `${command} --help --no-color`; + try { + let { stdout, stderr } = await exec(commandLine, { cwd: rootPath }); + if (stderr) { + getOutputChannel().appendLine(stderr); + getOutputChannel().show(true); + } + let result: vscode.Task[] = []; + if (stdout) { + // grunt lists tasks as follows (description is wrapped into a new line if too long): + // ... + // Available tasks + // uglify Minify files with UglifyJS. * + // jshint Validate files with JSHint. * + // test Alias for "jshint", "qunit" tasks. + // default Alias for "jshint", "qunit", "concat", "uglify" tasks. + // long Alias for "eslint", "qunit", "browserify", "sass", + // "autoprefixer", "uglify", tasks. + // + // Tasks run in the order specified + + let lines = stdout.split(/\r{0,1}\n/); + let tasksStart = false; + let tasksEnd = false; + for (let line of lines) { + if (line.length === 0) { + continue; } - } else if (tasksStart && !tasksEnd) { - if (line.indexOf('Tasks run in the order specified') === 0) { - tasksEnd = true; - } else { - let regExp = /^\s*(\S.*\S) \S/g; - let matches = regExp.exec(line); - if (matches && matches.length === 2) { - let name = matches[1]; - let kind: GruntTaskDefinition = { - type: 'grunt', - task: name - }; - let source = 'grunt'; - let task = name.indexOf(' ') === -1 - ? new vscode.Task(kind, name, source, new vscode.ShellExecution(`${command} ${name}`)) - : new vscode.Task(kind, name, source, new vscode.ShellExecution(`${command} "${name}"`)); - result.push(task); - let lowerCaseTaskName = name.toLowerCase(); - if (isBuildTask(lowerCaseTaskName)) { - task.group = vscode.TaskGroup.Build; - } else if (isTestTask(lowerCaseTaskName)) { - task.group = vscode.TaskGroup.Test; + if (!tasksStart && !tasksEnd) { + if (line.indexOf('Available tasks') === 0) { + tasksStart = true; + } + } else if (tasksStart && !tasksEnd) { + if (line.indexOf('Tasks run in the order specified') === 0) { + tasksEnd = true; + } else { + let regExp = /^\s*(\S.*\S) \S/g; + let matches = regExp.exec(line); + if (matches && matches.length === 2) { + let name = matches[1]; + let kind: GruntTaskDefinition = { + type: 'grunt', + task: name + }; + let source = 'grunt'; + let options: vscode.ShellExecutionOptions = { cwd: this.workspaceFolder.uri.fsPath }; + let task = name.indexOf(' ') === -1 + ? new vscode.Task(kind, this.workspaceFolder, name, source, new vscode.ShellExecution(`${command} ${name}`, options)) + : new vscode.Task(kind, this.workspaceFolder, name, source, new vscode.ShellExecution(`${command} "${name}"`, options)); + result.push(task); + let lowerCaseTaskName = name.toLowerCase(); + if (isBuildTask(lowerCaseTaskName)) { + task.group = vscode.TaskGroup.Build; + } else if (isTestTask(lowerCaseTaskName)) { + task.group = vscode.TaskGroup.Test; + } } } } } } + return result; + } catch (err) { + let channel = getOutputChannel(); + if (err.stderr) { + channel.appendLine(err.stderr); + } + if (err.stdout) { + channel.appendLine(err.stdout); + } + channel.appendLine(localize('execFailed', 'Auto detecting Grunt failed with error: {0}', err.error ? err.error.toString() : 'unknown')); + channel.show(true); + return emptyTasks; } - return result; - } catch (err) { - let channel = getOutputChannel(); - if (err.stderr) { - channel.appendLine(err.stderr); - } - if (err.stdout) { - channel.appendLine(err.stdout); - } - channel.appendLine(localize('execFailed', 'Auto detecting Grunt failed with error: {0}', err.error ? err.error.toString() : 'unknown')); - channel.show(true); - return emptyTasks; } + + public dispose() { + this.promise = undefined; + if (this.fileWatcher) { + this.fileWatcher.dispose(); + } + } +} + +class TaskDetector { + + private taskProvider: vscode.Disposable | undefined; + private detectors: Map = new Map(); + private promise: Promise | undefined; + + constructor() { + } + + public start(): void { + let folders = vscode.workspace.workspaceFolders; + if (folders) { + this.updateWorkspaceFolders(folders, []); + } + vscode.workspace.onDidChangeWorkspaceFolders((event) => this.updateWorkspaceFolders(event.added, event.removed)); + vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this); + } + + public dispose(): void { + if (this.taskProvider) { + this.taskProvider.dispose(); + this.taskProvider = undefined; + } + this.detectors.clear(); + this.promise = undefined; + } + + private updateWorkspaceFolders(added: vscode.WorkspaceFolder[], removed: vscode.WorkspaceFolder[]): void { + let changed = false; + for (let remove of removed) { + let detector = this.detectors.get(remove.uri.toString()); + if (detector) { + changed = true; + detector.dispose(); + this.detectors.delete(remove.uri.toString()); + } + } + for (let add of added) { + let detector = new FolderDetector(add); + if (detector.isEnabled()) { + changed = true; + this.detectors.set(add.uri.toString(), detector); + detector.start(); + } + } + if (changed) { + this.promise = undefined; + } + this.updateProvider(); + } + + private updateConfiguration(): void { + let changed = false; + for (let detector of this.detectors.values()) { + if (!detector.isEnabled()) { + changed = true; + detector.dispose(); + this.detectors.delete(detector.workspaceFolder.uri.toString()); + } + } + let folders = vscode.workspace.workspaceFolders; + if (folders) { + for (let folder of folders) { + if (!this.detectors.has(folder.uri.toString())) { + let detector = new FolderDetector(folder); + if (detector.isEnabled()) { + changed = true; + this.detectors.set(folder.uri.toString(), detector); + detector.start(); + } + } + } + } + if (changed) { + this.promise = undefined; + } + this.updateProvider(); + } + + private updateProvider(): void { + if (!this.taskProvider && this.detectors.size > 0) { + this.taskProvider = vscode.workspace.registerTaskProvider('gulp', { + provideTasks: () => { + return this.getTasks(); + }, + resolveTask(_task: vscode.Task): vscode.Task | undefined { + return undefined; + } + }); + } + else if (this.taskProvider && this.detectors.size === 0) { + this.taskProvider.dispose(); + this.taskProvider = undefined; + this.promise = undefined; + } + } + + public getTasks(): Promise { + if (!this.promise) { + this.promise = this.computeTasks(); + } + return this.promise; + } + + private computeTasks(): Promise { + if (this.detectors.size === 0) { + return Promise.resolve([]); + } else if (this.detectors.size === 1) { + return this.detectors.values().next().value.getTasks(); + } else { + let promises: Promise[] = []; + for (let detector of this.detectors.values()) { + promises.push(detector.getTasks().then((value) => value, () => [])); + } + return Promise.all(promises).then((values) => { + let result: vscode.Task[] = []; + for (let tasks of values) { + if (tasks && tasks.length > 0) { + result.push(...tasks); + } + } + return result; + }); + } + } +} + +let detector: TaskDetector; +export function activate(_context: vscode.ExtensionContext): void { + detector = new TaskDetector(); + detector.start(); +} + +export function deactivate(): void { + detector.dispose(); } \ No newline at end of file diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 235f3e2d138..498384f3fa7 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -31,6 +31,7 @@ "title": "Gulp", "properties": { "gulp.autoDetect": { + "scope": "resource", "type": "string", "enum": [ "off", diff --git a/extensions/gulp/src/main.ts b/extensions/gulp/src/main.ts index 24d7370cf13..127e07afd64 100644 --- a/extensions/gulp/src/main.ts +++ b/extensions/gulp/src/main.ts @@ -13,49 +13,6 @@ import * as nls from 'vscode-nls'; const localize = nls.config(process.env.VSCODE_NLS_CONFIG)(); type AutoDetect = 'on' | 'off'; -let taskProvider: vscode.Disposable | undefined; - -export function activate(_context: vscode.ExtensionContext): void { - let workspaceRoot = vscode.workspace.rootPath; - if (!workspaceRoot) { - return; - } - let pattern = path.join(workspaceRoot, 'gulpfile{.babel.js,.js}'); - let gulpPromise: Thenable | undefined = undefined; - let fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); - fileWatcher.onDidChange(() => gulpPromise = undefined); - fileWatcher.onDidCreate(() => gulpPromise = undefined); - fileWatcher.onDidDelete(() => gulpPromise = undefined); - - function onConfigurationChanged() { - let autoDetect = vscode.workspace.getConfiguration('gulp').get('autoDetect'); - if (taskProvider && autoDetect === 'off') { - gulpPromise = undefined; - taskProvider.dispose(); - taskProvider = undefined; - } else if (!taskProvider && autoDetect === 'on') { - taskProvider = vscode.workspace.registerTaskProvider('gulp', { - provideTasks: () => { - if (!gulpPromise) { - gulpPromise = getGulpTasks(); - } - return gulpPromise; - }, - resolveTask(_task: vscode.Task): vscode.Task | undefined { - return undefined; - } - }); - } - } - vscode.workspace.onDidChangeConfiguration(onConfigurationChanged); - onConfigurationChanged(); -} - -export function deactivate(): void { - if (taskProvider) { - taskProvider.dispose(); - } -} function exists(file: string): Promise { return new Promise((resolve, _reject) => { @@ -76,19 +33,6 @@ function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: strin }); } -let _channel: vscode.OutputChannel; -function getOutputChannel(): vscode.OutputChannel { - if (!_channel) { - _channel = vscode.window.createOutputChannel('Gulp Auto Detection'); - } - return _channel; -} - -interface GulpTaskDefinition extends vscode.TaskDefinition { - task: string; - file?: string; -} - const buildNames: string[] = ['build', 'compile', 'watch']; function isBuildTask(name: string): boolean { for (let buildName of buildNames) { @@ -109,69 +53,259 @@ function isTestTask(name: string): boolean { return false; } -async function getGulpTasks(): Promise { - let workspaceRoot = vscode.workspace.rootPath; - let emptyTasks: vscode.Task[] = []; - if (!workspaceRoot) { - return emptyTasks; +let _channel: vscode.OutputChannel; +function getOutputChannel(): vscode.OutputChannel { + if (!_channel) { + _channel = vscode.window.createOutputChannel('Gulp Auto Detection'); } - let gulpfile = path.join(workspaceRoot, 'gulpfile.js'); - if (!await exists(gulpfile)) { - gulpfile = path.join(workspaceRoot, 'gulpfile.babel.js'); - if (! await exists(gulpfile)) { + return _channel; +} + +interface GulpTaskDefinition extends vscode.TaskDefinition { + task: string; + file?: string; +} + +class FolderDetector { + + private fileWatcher: vscode.FileSystemWatcher; + private promise: Thenable | undefined; + + constructor(private _workspaceFolder: vscode.WorkspaceFolder) { + } + + public get workspaceFolder(): vscode.WorkspaceFolder { + return this._workspaceFolder; + } + + public isEnabled(): boolean { + return vscode.workspace.getConfiguration('gulp', this._workspaceFolder.uri).get('autoDetect') === 'on'; + } + + public start(): void { + let pattern = path.join(this._workspaceFolder.uri.fsPath, 'gulpfile{.babel.js,.js}'); + this.fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); + this.fileWatcher.onDidChange(() => this.promise = undefined); + this.fileWatcher.onDidCreate(() => this.promise = undefined); + this.fileWatcher.onDidDelete(() => this.promise = undefined); + } + + public async getTasks(): Promise { + if (!this.promise) { + this.promise = this.computeTasks(); + } + return this.promise; + } + + private async computeTasks(): Promise { + let rootPath = this._workspaceFolder.uri.scheme === 'file' ? this._workspaceFolder.uri.fsPath : undefined; + let emptyTasks: vscode.Task[] = []; + if (!rootPath) { + return emptyTasks; + } + let gulpfile = path.join(rootPath, 'gulpfile.js'); + if (!await exists(gulpfile)) { + gulpfile = path.join(rootPath, 'gulpfile.babel.js'); + if (! await exists(gulpfile)) { + return emptyTasks; + } + } + + let gulpCommand: string; + let platform = process.platform; + if (platform === 'win32' && await exists(path.join(rootPath!, 'node_modules', '.bin', 'gulp.cmd'))) { + gulpCommand = path.join('.', 'node_modules', '.bin', 'gulp.cmd'); + } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(rootPath!, 'node_modules', '.bin', 'gulp'))) { + gulpCommand = path.join('.', 'node_modules', '.bin', 'gulp'); + } else { + gulpCommand = 'gulp'; + } + + let commandLine = `${gulpCommand} --tasks-simple --no-color`; + try { + let { stdout, stderr } = await exec(commandLine, { cwd: rootPath }); + if (stderr && stderr.length > 0) { + getOutputChannel().appendLine(stderr); + getOutputChannel().show(true); + } + let result: vscode.Task[] = []; + if (stdout) { + let lines = stdout.split(/\r{0,1}\n/); + for (let line of lines) { + if (line.length === 0) { + continue; + } + let kind: GulpTaskDefinition = { + type: 'gulp', + task: line + }; + let options: vscode.ShellExecutionOptions = { cwd: this.workspaceFolder.uri.fsPath }; + let task = new vscode.Task(kind, this.workspaceFolder, line, 'gulp', new vscode.ShellExecution(`${gulpCommand} ${line}`, options)); + result.push(task); + let lowerCaseLine = line.toLowerCase(); + if (isBuildTask(lowerCaseLine)) { + task.group = vscode.TaskGroup.Build; + } else if (isTestTask(lowerCaseLine)) { + task.group = vscode.TaskGroup.Test; + } + } + } + return result; + } catch (err) { + let channel = getOutputChannel(); + if (err.stderr) { + channel.appendLine(err.stderr); + } + if (err.stdout) { + channel.appendLine(err.stdout); + } + channel.appendLine(localize('execFailed', 'Auto detecting gulp failed with error: {0}', err.error ? err.error.toString() : 'unknown')); + channel.show(true); return emptyTasks; } } - let gulpCommand: string; - let platform = process.platform; - if (platform === 'win32' && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'gulp.cmd'))) { - gulpCommand = path.join('.', 'node_modules', '.bin', 'gulp.cmd'); - } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'gulp'))) { - gulpCommand = path.join('.', 'node_modules', '.bin', 'gulp'); - } else { - gulpCommand = 'gulp'; + public dispose() { + this.promise = undefined; + if (this.fileWatcher) { + this.fileWatcher.dispose(); + } + } +} + +class TaskDetector { + + private taskProvider: vscode.Disposable | undefined; + private detectors: Map = new Map(); + private promise: Promise | undefined; + + constructor() { } - let commandLine = `${gulpCommand} --tasks-simple --no-color`; - try { - let { stdout, stderr } = await exec(commandLine, { cwd: workspaceRoot }); - if (stderr && stderr.length > 0) { - getOutputChannel().appendLine(stderr); - getOutputChannel().show(true); + public start(): void { + let folders = vscode.workspace.workspaceFolders; + if (folders) { + this.updateWorkspaceFolders(folders, []); } - let result: vscode.Task[] = []; - if (stdout) { - let lines = stdout.split(/\r{0,1}\n/); - for (let line of lines) { - if (line.length === 0) { - continue; - } - let kind: GulpTaskDefinition = { - type: 'gulp', - task: line - }; - let task = new vscode.Task(kind, line, 'gulp', new vscode.ShellExecution(`${gulpCommand} ${line}`)); - result.push(task); - let lowerCaseLine = line.toLowerCase(); - if (isBuildTask(lowerCaseLine)) { - task.group = vscode.TaskGroup.Build; - } else if (isTestTask(lowerCaseLine)) { - task.group = vscode.TaskGroup.Test; + vscode.workspace.onDidChangeWorkspaceFolders((event) => this.updateWorkspaceFolders(event.added, event.removed)); + vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this); + } + + public dispose(): void { + if (this.taskProvider) { + this.taskProvider.dispose(); + this.taskProvider = undefined; + } + this.detectors.clear(); + this.promise = undefined; + } + + private updateWorkspaceFolders(added: vscode.WorkspaceFolder[], removed: vscode.WorkspaceFolder[]): void { + let changed = false; + for (let remove of removed) { + let detector = this.detectors.get(remove.uri.toString()); + if (detector) { + changed = true; + detector.dispose(); + this.detectors.delete(remove.uri.toString()); + } + } + for (let add of added) { + let detector = new FolderDetector(add); + if (detector.isEnabled()) { + changed = true; + this.detectors.set(add.uri.toString(), detector); + detector.start(); + } + } + if (changed) { + this.promise = undefined; + } + this.updateProvider(); + } + + private updateConfiguration(): void { + let changed = false; + for (let detector of this.detectors.values()) { + if (!detector.isEnabled()) { + changed = true; + detector.dispose(); + this.detectors.delete(detector.workspaceFolder.uri.toString()); + } + } + let folders = vscode.workspace.workspaceFolders; + if (folders) { + for (let folder of folders) { + if (!this.detectors.has(folder.uri.toString())) { + let detector = new FolderDetector(folder); + if (detector.isEnabled()) { + changed = true; + this.detectors.set(folder.uri.toString(), detector); + detector.start(); + } } } } - return result; - } catch (err) { - let channel = getOutputChannel(); - if (err.stderr) { - channel.appendLine(err.stderr); + if (changed) { + this.promise = undefined; } - if (err.stdout) { - channel.appendLine(err.stdout); - } - channel.appendLine(localize('execFailed', 'Auto detecting gulp failed with error: {0}', err.error ? err.error.toString() : 'unknown')); - channel.show(true); - return emptyTasks; + this.updateProvider(); } + + private updateProvider(): void { + if (!this.taskProvider && this.detectors.size > 0) { + this.taskProvider = vscode.workspace.registerTaskProvider('gulp', { + provideTasks: () => { + return this.getTasks(); + }, + resolveTask(_task: vscode.Task): vscode.Task | undefined { + return undefined; + } + }); + } + else if (this.taskProvider && this.detectors.size === 0) { + this.taskProvider.dispose(); + this.taskProvider = undefined; + this.promise = undefined; + } + } + + public getTasks(): Promise { + if (!this.promise) { + this.promise = this.computeTasks(); + } + return this.promise; + } + + private computeTasks(): Promise { + if (this.detectors.size === 0) { + return Promise.resolve([]); + } else if (this.detectors.size === 1) { + return this.detectors.values().next().value.getTasks(); + } else { + let promises: Promise[] = []; + for (let detector of this.detectors.values()) { + promises.push(detector.getTasks().then((value) => value, () => [])); + } + return Promise.all(promises).then((values) => { + let result: vscode.Task[] = []; + for (let tasks of values) { + if (tasks && tasks.length > 0) { + result.push(...tasks); + } + } + return result; + }); + } + } +} + +let detector: TaskDetector; +export function activate(_context: vscode.ExtensionContext): void { + detector = new TaskDetector(); + detector.start(); +} + +export function deactivate(): void { + detector.dispose(); } \ No newline at end of file diff --git a/extensions/json/test/colorize-results/test_json.json b/extensions/json/test/colorize-results/test_json.json index facb8d52a57..f5295bcf70a 100644 --- a/extensions/json/test/colorize-results/test_json.json +++ b/extensions/json/test/colorize-results/test_json.json @@ -388,8 +388,8 @@ "c": "\\u0056", "t": "source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json constant.character.escape.json", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" diff --git a/extensions/make/test/colorize-results/makefile.json b/extensions/make/test/colorize-results/makefile.json index 1fb93bab87c..94d9b456703 100644 --- a/extensions/make/test/colorize-results/makefile.json +++ b/extensions/make/test/colorize-results/makefile.json @@ -135,8 +135,8 @@ "c": "\\", "t": "source.makefile meta.scope.target.makefile meta.scope.prerequisites.makefile constant.character.escape.continuation.makefile", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", "hc_black": "constant.character: #569CD6" @@ -278,8 +278,8 @@ "c": "\\", "t": "source.makefile meta.scope.target.makefile meta.scope.prerequisites.makefile constant.character.escape.continuation.makefile", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", "hc_black": "constant.character: #569CD6" @@ -322,8 +322,8 @@ "c": "\\", "t": "source.makefile meta.scope.target.makefile meta.scope.prerequisites.makefile comment.line.number-sign.makefile constant.character.escape.continuation.makefile", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "comment: #608B4E", "light_vs": "comment: #008000", "hc_black": "constant.character: #569CD6" @@ -377,8 +377,8 @@ "c": "\\", "t": "source.makefile comment.line.number-sign.makefile constant.character.escape.continuation.makefile", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "comment: #608B4E", "light_vs": "comment: #008000", "hc_black": "constant.character: #569CD6" @@ -553,8 +553,8 @@ "c": "\\", "t": "source.makefile meta.scope.target.makefile meta.scope.prerequisites.makefile constant.character.escape.continuation.makefile", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", "hc_black": "constant.character: #569CD6" diff --git a/extensions/perl/test/colorize-results/test2_pl.json b/extensions/perl/test/colorize-results/test2_pl.json index af312ff8930..bf690458af4 100644 --- a/extensions/perl/test/colorize-results/test2_pl.json +++ b/extensions/perl/test/colorize-results/test2_pl.json @@ -1301,8 +1301,8 @@ "c": "\\d\\d\\d", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -1323,8 +1323,8 @@ "c": "\\d\\d", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -1345,8 +1345,8 @@ "c": "\\d\\d", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -1774,8 +1774,8 @@ "c": "\\d", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" diff --git a/extensions/perl/test/colorize-results/test_pl.json b/extensions/perl/test/colorize-results/test_pl.json index 8062166dc5c..e6551c2c639 100644 --- a/extensions/perl/test/colorize-results/test_pl.json +++ b/extensions/perl/test/colorize-results/test_pl.json @@ -388,8 +388,8 @@ "c": "\\s", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -410,8 +410,8 @@ "c": "\\s", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -432,8 +432,8 @@ "c": "\\(", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -454,8 +454,8 @@ "c": "\\)", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -476,8 +476,8 @@ "c": "\\)", "t": "source.perl string.regexp.find.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -674,8 +674,8 @@ "c": "\\n", "t": "source.perl string.quoted.double.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -949,8 +949,8 @@ "c": "\\n", "t": "source.perl string.quoted.double.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -1444,8 +1444,8 @@ "c": "\\n", "t": "source.perl string.quoted.double.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -2280,8 +2280,8 @@ "c": "\\n", "t": "source.perl string.quoted.double.perl constant.character.escape.perl", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" diff --git a/extensions/php/package.json b/extensions/php/package.json index e3b313bb321..3f50c17669b 100644 --- a/extensions/php/package.json +++ b/extensions/php/package.json @@ -27,7 +27,7 @@ "PHP", "php" ], - "firstLine": "^#!/.*\\bphp\\b", + "firstLine": "^#!/.*\\bphp\\b", "mimetypes": [ "application/x-php" ], diff --git a/extensions/php/test/colorize-results/issue-28354_php.json b/extensions/php/test/colorize-results/issue-28354_php.json index 5f1b87f3834..10314265464 100644 --- a/extensions/php/test/colorize-results/issue-28354_php.json +++ b/extensions/php/test/colorize-results/issue-28354_php.json @@ -278,8 +278,8 @@ "c": "\\'", "t": "text.html.php meta.embedded.block.html source.js meta.embedded.block.php source.php string.quoted.single.php constant.character.escape.php", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -377,8 +377,8 @@ "c": "\\'", "t": "text.html.php meta.embedded.block.html source.js meta.embedded.block.php source.php string.quoted.single.php constant.character.escape.php", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" diff --git a/extensions/python/test/colorize-results/test_py.json b/extensions/python/test/colorize-results/test_py.json index ef8d364c52b..235958f078f 100644 --- a/extensions/python/test/colorize-results/test_py.json +++ b/extensions/python/test/colorize-results/test_py.json @@ -5294,8 +5294,8 @@ "c": "(", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python support.other.parenthesis.regexp punctuation.parenthesis.begin.regexp", "r": { - "dark_plus": "support.other.parenthesis.regexp: #D7BA7D", - "light_plus": "support.other.parenthesis.regexp: #FF0000", + "dark_plus": "support.other.parenthesis.regexp: #CE9178", + "light_plus": "support.other.parenthesis.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5305,8 +5305,8 @@ "c": "[", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python meta.character.set.regexp punctuation.character.set.begin.regexp constant.other.set.regexp", "r": { - "dark_plus": "meta.character.set.regexp: #D7BA7D", - "light_plus": "meta.character.set.regexp: #FF0000", + "dark_plus": "punctuation.character.set.begin.regexp: #CE9178", + "light_plus": "punctuation.character.set.begin.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5316,8 +5316,8 @@ "c": "0-9-", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python meta.character.set.regexp constant.character.set.regexp", "r": { - "dark_plus": "constant.character: #569CD6", - "light_plus": "constant.character: #0000FF", + "dark_plus": "constant.character.set.regexp: #D16969", + "light_plus": "constant.character.set.regexp: #811F3F", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -5327,8 +5327,8 @@ "c": "]", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python meta.character.set.regexp punctuation.character.set.end.regexp constant.other.set.regexp", "r": { - "dark_plus": "meta.character.set.regexp: #D7BA7D", - "light_plus": "meta.character.set.regexp: #FF0000", + "dark_plus": "punctuation.character.set.end.regexp: #CE9178", + "light_plus": "punctuation.character.set.end.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5338,8 +5338,8 @@ "c": "*", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -5349,8 +5349,8 @@ "c": ")", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python support.other.parenthesis.regexp punctuation.parenthesis.end.regexp", "r": { - "dark_plus": "support.other.parenthesis.regexp: #D7BA7D", - "light_plus": "support.other.parenthesis.regexp: #FF0000", + "dark_plus": "support.other.parenthesis.regexp: #CE9178", + "light_plus": "support.other.parenthesis.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5371,8 +5371,8 @@ "c": "*", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -5382,8 +5382,8 @@ "c": "(", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python support.other.parenthesis.regexp punctuation.parenthesis.begin.regexp", "r": { - "dark_plus": "support.other.parenthesis.regexp: #D7BA7D", - "light_plus": "support.other.parenthesis.regexp: #FF0000", + "dark_plus": "support.other.parenthesis.regexp: #CE9178", + "light_plus": "support.other.parenthesis.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5393,8 +5393,8 @@ "c": "[", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python meta.character.set.regexp punctuation.character.set.begin.regexp constant.other.set.regexp", "r": { - "dark_plus": "meta.character.set.regexp: #D7BA7D", - "light_plus": "meta.character.set.regexp: #FF0000", + "dark_plus": "punctuation.character.set.begin.regexp: #CE9178", + "light_plus": "punctuation.character.set.begin.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5404,8 +5404,8 @@ "c": "A-Za-z", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python meta.character.set.regexp constant.character.set.regexp", "r": { - "dark_plus": "constant.character: #569CD6", - "light_plus": "constant.character: #0000FF", + "dark_plus": "constant.character.set.regexp: #D16969", + "light_plus": "constant.character.set.regexp: #811F3F", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -5415,8 +5415,8 @@ "c": "]", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python meta.character.set.regexp punctuation.character.set.end.regexp constant.other.set.regexp", "r": { - "dark_plus": "meta.character.set.regexp: #D7BA7D", - "light_plus": "meta.character.set.regexp: #FF0000", + "dark_plus": "punctuation.character.set.end.regexp: #CE9178", + "light_plus": "punctuation.character.set.end.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5426,8 +5426,8 @@ "c": "+", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -5437,8 +5437,8 @@ "c": ")", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python support.other.parenthesis.regexp punctuation.parenthesis.end.regexp", "r": { - "dark_plus": "support.other.parenthesis.regexp: #D7BA7D", - "light_plus": "support.other.parenthesis.regexp: #FF0000", + "dark_plus": "support.other.parenthesis.regexp: #CE9178", + "light_plus": "support.other.parenthesis.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5470,8 +5470,8 @@ "c": "+", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -5481,8 +5481,8 @@ "c": "(", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python support.other.parenthesis.regexp punctuation.parenthesis.begin.regexp", "r": { - "dark_plus": "support.other.parenthesis.regexp: #D7BA7D", - "light_plus": "support.other.parenthesis.regexp: #FF0000", + "dark_plus": "support.other.parenthesis.regexp: #CE9178", + "light_plus": "support.other.parenthesis.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -5503,8 +5503,8 @@ "c": "*", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python keyword.operator.quantifier.regexp", "r": { - "dark_plus": "keyword.operator.quantifier.regexp: #D4D4D4", - "light_plus": "keyword.operator.quantifier.regexp: #0000FF", + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", "dark_vs": "keyword.operator: #D4D4D4", "light_vs": "keyword.operator: #000000", "hc_black": "keyword.operator: #D4D4D4" @@ -5514,8 +5514,8 @@ "c": ")", "t": "source.python meta.function-call.python meta.function-call.arguments.python string.regexp.quoted.single.python support.other.parenthesis.regexp punctuation.parenthesis.end.regexp", "r": { - "dark_plus": "support.other.parenthesis.regexp: #D7BA7D", - "light_plus": "support.other.parenthesis.regexp: #FF0000", + "dark_plus": "support.other.parenthesis.regexp: #CE9178", + "light_plus": "support.other.parenthesis.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -6504,8 +6504,8 @@ "c": "[", "t": "source.python string.regexp.quoted.multi.python meta.character.set.regexp punctuation.character.set.begin.regexp constant.other.set.regexp", "r": { - "dark_plus": "meta.character.set.regexp: #D7BA7D", - "light_plus": "meta.character.set.regexp: #FF0000", + "dark_plus": "punctuation.character.set.begin.regexp: #CE9178", + "light_plus": "punctuation.character.set.begin.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969" @@ -6515,8 +6515,8 @@ "c": "1,2)`` leads to", "t": "source.python string.regexp.quoted.multi.python meta.character.set.regexp constant.character.set.regexp", "r": { - "dark_plus": "constant.character: #569CD6", - "light_plus": "constant.character: #0000FF", + "dark_plus": "constant.character.set.regexp: #D16969", + "light_plus": "constant.character.set.regexp: #811F3F", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" diff --git a/extensions/ruby/test/colorize-results/test_rb.json b/extensions/ruby/test/colorize-results/test_rb.json index 005f04f37c3..996dbbf0192 100644 --- a/extensions/ruby/test/colorize-results/test_rb.json +++ b/extensions/ruby/test/colorize-results/test_rb.json @@ -2500,8 +2500,8 @@ "c": "\\d", "t": "source.ruby string.regexp.classic.ruby meta.group.regexp.ruby constant.character.escape.ruby", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -2522,8 +2522,8 @@ "c": "\\.\\d", "t": "source.ruby string.regexp.classic.ruby meta.group.regexp.ruby constant.character.escape.ruby", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -2544,8 +2544,8 @@ "c": "\\.\\d", "t": "source.ruby string.regexp.classic.ruby meta.group.regexp.ruby constant.character.escape.ruby", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" @@ -2577,8 +2577,8 @@ "c": "\\.\\d", "t": "source.ruby string.regexp.classic.ruby meta.group.regexp.ruby meta.group.regexp.ruby constant.character.escape.ruby", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "constant.character: #569CD6" diff --git a/extensions/scss/test/colorize-results/test_scss.json b/extensions/scss/test/colorize-results/test_scss.json index 301d6fc35fd..cc598d4c7a2 100644 --- a/extensions/scss/test/colorize-results/test_scss.json +++ b/extensions/scss/test/colorize-results/test_scss.json @@ -20716,8 +20716,8 @@ "c": "\\\\", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.property-value.scss string.quoted.single.scss constant.character.escape.scss", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -20837,8 +20837,8 @@ "c": "\\'", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-value.scss string.quoted.single.scss constant.character.escape.scss", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -20914,8 +20914,8 @@ "c": "\\\"", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-value.scss string.quoted.double.scss constant.character.escape.scss", "r": { - "dark_plus": "constant.character.escape: #C586C0", - "light_plus": "constant.character.escape: #FF0000", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index 55052a69d66..6fdfb2263ce 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -103,28 +103,44 @@ } }, { + "name": "Regular expression groups", "scope": [ "punctuation.definition.group.regexp", "punctuation.definition.group.assertion.regexp", "punctuation.definition.character-class.regexp", + "punctuation.character.set.begin.regexp", + "punctuation.character.set.end.regexp", "keyword.operator.negation.regexp", - "support.other.parenthesis.regexp", - "meta.character.set.regexp" + "support.other.parenthesis.regexp" ], "settings": { - "foreground": "#d7ba7d" + "foreground": "#CE9178" + } + }, + { + "scope": [ + "constant.character.character-class.regexp", + "constant.other.character-class.set.regexp", + "constant.other.character-class.regexp", + "constant.character.set.regexp" + ], + "settings": { + "foreground": "#d16969" + } + }, + { + "scope": [ + "keyword.operator.or.regexp", + "keyword.control.anchor.regexp" + ], + "settings": { + "foreground": "#DCDCAA" } }, { "scope": "keyword.operator.quantifier.regexp", "settings": { - "foreground": "#d4d4d4" - } - }, - { - "scope": "keyword.control.anchor.regexp", - "settings": { - "foreground": "#C586C0" + "foreground": "#d7ba7d" } }, { @@ -136,9 +152,8 @@ { "scope": "constant.character.escape", "settings": { - "foreground": "#C586C0" + "foreground": "#d7ba7d" } } - ] } \ No newline at end of file diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index b533627c9f2..3d5775ecec5 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -103,28 +103,44 @@ } }, { + "name": "Regular expression groups", "scope": [ "punctuation.definition.group.regexp", "punctuation.definition.group.assertion.regexp", "punctuation.definition.character-class.regexp", + "punctuation.character.set.begin.regexp", + "punctuation.character.set.end.regexp", "keyword.operator.negation.regexp", - "support.other.parenthesis.regexp", - "meta.character.set.regexp" + "support.other.parenthesis.regexp" ], "settings": { - "foreground": "#ff0000" + "foreground": "#d16969" + } + }, + { + "scope": [ + "constant.character.character-class.regexp", + "constant.other.character-class.set.regexp", + "constant.other.character-class.regexp", + "constant.character.set.regexp" + ], + "settings": { + "foreground": "#811f3f" } }, { "scope": "keyword.operator.quantifier.regexp", "settings": { - "foreground": "#0000ff" + "foreground": "#000000" } }, { - "scope": "keyword.control.anchor.regexp", + "scope": [ + "keyword.operator.or.regexp", + "keyword.control.anchor.regexp" + ], "settings": { - "foreground": "#09885a" + "foreground": "#ff0000" } }, { @@ -136,8 +152,9 @@ { "scope": "constant.character.escape", "settings": { - "foreground": "#ff0000" + "foreground": "#a31515" } } + ] } \ No newline at end of file diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 043e9be8be9..4dfeff378ee 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -56,6 +56,7 @@ export default class URI { private static _empty = ''; private static _slash = '/'; private static _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + private static _driveLetter = /^[a-zA-Z]:/; private static _driveLetterPath = /^\/[a-zA-Z]:/; private static _upperCaseDrive = /^(\/)?([A-Z]:)/; @@ -212,19 +213,25 @@ export default class URI { let idx = path.indexOf(URI._slash, 2); if (idx === -1) { authority = path.substring(2); - path = URI._empty; + path = URI._slash; } else { authority = path.substring(2, idx); path = path.substring(idx); } } - // Ensure that path starts with a slash - // or that it is at least a slash - if (path[0] !== URI._slash) { + // absolute windows paths get another slash + if (URI._driveLetter.test(path)) { path = URI._slash + path; } + // path must be absolute because we cannot format them + // otherwise: `file:./foo` -> file://./foo -> `{ authority: '.', path: 'foo' }` + // https://tools.ietf.org/html/rfc8089#section-2 + else if (path[0] !== URI._slash) { + throw new Error('[UriError]: relative paths are not supported.'); + } + return new URI('file', authority, path, URI._empty, URI._empty); } @@ -301,8 +308,20 @@ export default class URI { parts.push('//'); } if (authority) { + let idx = authority.indexOf('@'); + if (idx !== -1) { + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); + if (idx === -1) { + parts.push(encoder(userinfo)); + } else { + parts.push(encoder(userinfo.substr(0, idx)), ':', encoder(userinfo.substr(idx + 1))); + } + parts.push('@'); + } authority = authority.toLowerCase(); - let idx = authority.indexOf(':'); + idx = authority.indexOf(':'); if (idx === -1) { parts.push(encoder(authority)); } else { diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 1e8340f98c7..df2d989d26c 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -38,13 +38,11 @@ suite('URI', () => { assert.equal(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); assert.equal(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); assert.equal(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); } else { assert.equal(URI.file('c:/win/path').fsPath, 'c:/win/path'); assert.equal(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); assert.equal(URI.file('C:/win/path').fsPath, 'c:/win/path'); assert.equal(URI.file('/c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('./c/win/path').fsPath, '/./c/win/path'); } }); @@ -318,27 +316,12 @@ suite('URI', () => { test('URI#file, no path-is-uri check', () => { // we don't complain here - let value = URI.file('file://path/to/file'); + let value = URI.file('/file://path/to/file'); assert.equal(value.scheme, 'file'); assert.equal(value.authority, ''); assert.equal(value.path, '/file://path/to/file'); }); - test('URI#file, always slash', () => { - - var value = URI.file('a.file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); - - value = URI.parse(value.toString()); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); - }); - test('URI.toString, only scheme and query', () => { var value = URI.parse('stuff:?qüery'); assert.equal(value.toString(), 'stuff:?q%C3%BCery'); @@ -368,6 +351,23 @@ suite('URI', () => { assert.equal(value.toString(), 'http://l%C3%B6calhost:8080/far'); }); + test('URI#toString, user information in authority', () => { + var value = URI.parse('http://foo:bar@localhost/far'); + assert.equal(value.toString(), 'http://foo:bar@localhost/far'); + + value = URI.parse('http://foo@localhost/far'); + assert.equal(value.toString(), 'http://foo@localhost/far'); + + value = URI.parse('http://foo:bAr@localhost:8080/far'); + assert.equal(value.toString(), 'http://foo:bAr@localhost:8080/far'); + + value = URI.parse('http://foo@localhost:8080/far'); + assert.equal(value.toString(), 'http://foo@localhost:8080/far'); + + value = URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); + assert.equal(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); + }); + test('correctFileUriToFilePath2', () => { var test = (input: string, expected: string) => { @@ -410,13 +410,36 @@ suite('URI', () => { assert.equal(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); }); + test('class URI cannot represent relative file paths, #34449', function () { + const uri = URI.parse('file:./relative/path'); + assert.equal(uri.scheme, 'file'); + assert.equal(uri.authority, ''); + assert.equal(uri.path, './relative/path'); + + assert.equal(uri.toString(), 'file://./relative/path'); + + // this is asymetric to be spec-compliant + const uri2 = URI.parse(uri.toString()); + assert.equal(uri2.authority, '.'); + assert.equal(uri2.path, '/relative/path'); + }); + + test('class URI cannot represent relative file paths, #34449', function () { + + const path = '/foo/bar'; + assert.equal(URI.file(path).path, path); + + // no relative paths + assert.throws(() => URI.file('foo/bar')); + assert.throws(() => URI.file('./foo/bar')); + }); test('URI - (de)serialize', function () { var values = [ URI.parse('http://localhost:8080/far'), URI.file('c:\\test with %25\\c#code'), - URI.file('\\\\shäres\\path\\c#\\plugin.json'), + URI.file('//shäres/path/c#/plugin.json'), URI.parse('http://api/files/test.me?t=1234'), URI.parse('http://api/files/test.me?t=1234#fff'), URI.parse('http://api/files/test.me#fff'), diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index e04278782bf..5c6464d7db1 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -5,7 +5,7 @@ 'use strict'; -import * as path from 'path'; +import { basename, normalize, join, dirname } from 'path'; import * as fs from 'original-fs'; import { localize } from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; @@ -114,6 +114,7 @@ export class WindowsManager implements IWindowsMainService { private lastClosedWindowState: IWindowState; private fileDialog: FileDialog; + private workspacesManager: WorkspacesManager; private _onWindowReady = new Emitter(); onWindowReady: CommonEvent = this._onWindowReady.event; @@ -146,7 +147,9 @@ export class WindowsManager implements IWindowsMainService { @IInstantiationService private instantiationService: IInstantiationService ) { this.windowsState = this.storageService.getItem(WindowsManager.windowsStateStorageKey) || { openedWindows: [] }; + this.fileDialog = new FileDialog(environmentService, telemetryService, storageService, this); + this.workspacesManager = new WorkspacesManager(workspacesService, lifecycleService, backupService, environmentService, this); this.migrateLegacyWindowState(); } @@ -403,7 +406,7 @@ export class WindowsManager implements IWindowsMainService { workspacesToRestore.push(...this.workspacesService.getUntitledWorkspacesSync()); // collect from previous window session emptyToRestore = this.backupService.getEmptyWindowBackupPaths(); - emptyToRestore.push(...pathsToOpen.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => path.basename(w.backupPath))); // add empty windows with backupPath + emptyToRestore.push(...pathsToOpen.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => basename(w.backupPath))); // add empty windows with backupPath emptyToRestore = arrays.distinct(emptyToRestore); // prevent duplicates } @@ -932,7 +935,7 @@ export class WindowsManager implements IWindowsMainService { anyPath = parsedPath.path; } - const candidate = path.normalize(anyPath); + const candidate = normalize(anyPath); try { const candidateStat = fs.statSync(candidate); if (candidateStat) { @@ -1051,7 +1054,7 @@ export class WindowsManager implements IWindowsMainService { // For all other cases we first call into registerEmptyWindowBackupSync() to set it before // loading the window. if (options.emptyWindowBackupFolder) { - configuration.backupPath = path.join(this.environmentService.backupHome, options.emptyWindowBackupFolder); + configuration.backupPath = join(this.environmentService.backupHome, options.emptyWindowBackupFolder); } let window: CodeWindow; @@ -1291,93 +1294,19 @@ export class WindowsManager implements IWindowsMainService { }); } - public saveAndOpenWorkspace(window: CodeWindow, path: string): TPromise { - if (!window || !window.win || window.readyState !== ReadyState.READY || !window.openedWorkspace || !path) { - return TPromise.as(null); // return early if the window is not ready or disposed or does not have a workspace - } - - return this.doSaveAndOpenWorkspace(window, window.openedWorkspace, path); + public saveAndOpenWorkspace(win: CodeWindow, path: string): TPromise { + return this.workspacesManager.saveAndOpenWorkspace(win, path); } - public createAndOpenWorkspace(window: CodeWindow, folders?: string[], path?: string): TPromise { - if (!window || !window.win || window.readyState !== ReadyState.READY) { - return TPromise.as(null); // return early if the window is not ready or disposed - } - - return this.workspacesService.createWorkspace(folders).then(workspace => { - return this.doSaveAndOpenWorkspace(window, workspace, path); - }); + public createAndOpenWorkspace(win: CodeWindow, folders?: string[], path?: string): TPromise { + return this.workspacesManager.createAndOpenWorkspace(win, folders, path); } - private doSaveAndOpenWorkspace(window: CodeWindow, workspace: IWorkspaceIdentifier, path?: string): TPromise { - let savePromise: TPromise; - if (path) { - savePromise = this.workspacesService.saveWorkspace(workspace, path); - } else { - savePromise = TPromise.as(workspace); - } - - return savePromise.then(workspace => { - window.focus(); - - // Only open workspace when the window has not vetoed this - return this.lifecycleService.unload(window, UnloadReason.RELOAD, workspace).done(veto => { - if (!veto) { - - // Register window for backups and migrate current backups over - let backupPath: string; - if (window.config && !window.config.extensionDevelopmentPath) { - backupPath = this.backupService.registerWorkspaceBackupSync(workspace, window.config.backupPath); - } - - // Craft a new window configuration to use for the transition - const configuration: IWindowConfiguration = mixin({}, window.config); - configuration.folderPath = void 0; - configuration.workspace = workspace; - configuration.backupPath = backupPath; - - // Reload - window.reload(configuration); - } - }); - }); + public openWorkspace(win?: CodeWindow): void { + this.workspacesManager.openWorkspace(win); } - public openWorkspace(window: CodeWindow = this.getLastActiveWindow()): void { - let defaultPath: string; - if (window && window.openedWorkspace && !this.workspacesService.isUntitledWorkspace(window.openedWorkspace)) { - defaultPath = path.dirname(window.openedWorkspace.configPath); - } else { - defaultPath = this.getWorkspaceDialogDefaultPath(window ? (window.openedWorkspace || window.openedFolderPath) : void 0); - } - this.pickFileAndOpen({ - windowId: window ? window.id : void 0, - dialogOptions: { - buttonLabel: mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")), - title: localize('openWorkspaceTitle', "Open Workspace"), - filters: WORKSPACE_FILTER, - properties: ['openFile'], - defaultPath - } - }); - } - - private getWorkspaceDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { - let defaultPath: string; - if (workspace) { - if (isSingleFolderWorkspaceIdentifier(workspace)) { - defaultPath = path.dirname(workspace); - } else { - const resolvedWorkspace = this.workspacesService.resolveWorkspaceSync(workspace.configPath); - if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) { - defaultPath = path.dirname(resolvedWorkspace.folders[0].path); - } - } - } - - return defaultPath; - } private onBeforeWindowUnload(e: IWindowUnloadEvent): void { const windowClosing = (e.reason === UnloadReason.CLOSE); @@ -1399,74 +1328,8 @@ export class WindowsManager implements IWindowsMainService { return; // Windows/Linux: quits when last window is closed, so do not ask then } - this.promptToSaveUntitledWorkspace(e, workspace); - } - - private promptToSaveUntitledWorkspace(e: IWindowUnloadEvent, workspace: IWorkspaceIdentifier): void { - enum ConfirmResult { - SAVE, - DONT_SAVE, - CANCEL - } - - const save = { label: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), result: ConfirmResult.SAVE }; - const dontSave = { label: mnemonicButtonLabel(localize({ key: 'doNotSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save")), result: ConfirmResult.DONT_SAVE }; - const cancel = { label: localize('cancel', "Cancel"), result: ConfirmResult.CANCEL }; - - const buttons: { label: string; result: ConfirmResult; }[] = []; - if (isWindows) { - buttons.push(save, dontSave, cancel); - } else if (isLinux) { - buttons.push(dontSave, cancel, save); - } else { - buttons.push(save, cancel, dontSave); - } - - const options: Electron.MessageBoxOptions = { - title: this.environmentService.appNameLong, - message: localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"), - detail: localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."), - noLink: true, - type: 'warning', - buttons: buttons.map(button => button.label), - cancelId: buttons.indexOf(cancel) - }; - - if (isLinux) { - options.defaultId = 2; - } - - const res = dialog.showMessageBox(e.window.win, options); - - switch (buttons[res].result) { - - // Cancel: veto unload - case ConfirmResult.CANCEL: - e.veto(true); - break; - - // Don't Save: delete workspace - case ConfirmResult.DONT_SAVE: - this.workspacesService.deleteUntitledWorkspaceSync(workspace); - e.veto(false); - break; - - // Save: save workspace, but do not veto unload - case ConfirmResult.SAVE: { - const target = dialog.showSaveDialog(e.window.win, { - buttonLabel: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), - title: localize('saveWorkspace', "Save Workspace"), - filters: WORKSPACE_FILTER, - defaultPath: this.getWorkspaceDialogDefaultPath(workspace) - }); - - if (target) { - e.veto(this.workspacesService.saveWorkspace(workspace, target).then(() => false, () => false)); - } else { - e.veto(true); // keep veto if no target was provided - } - } - } + // Handle untitled workspaces with prompt as needed + this.workspacesManager.promptToSaveUntitledWorkspace(e, workspace); } public focusLastActive(cli: ParsedArgs, context: OpenContext): CodeWindow { @@ -1754,7 +1617,7 @@ class FileDialog { if (paths && paths.length > 0) { // Remember path in storage for next time - this.storageService.setItem(FileDialog.workingDirPickerStorageKey, path.dirname(paths[0])); + this.storageService.setItem(FileDialog.workingDirPickerStorageKey, dirname(paths[0])); // Return return clb(paths); @@ -1763,4 +1626,204 @@ class FileDialog { return clb(void (0)); }); } +} + +class WorkspacesManager { + + constructor( + private workspacesService: IWorkspacesMainService, + private lifecycleService: ILifecycleService, + private backupService: IBackupMainService, + private environmentService: IEnvironmentService, + private windowsMainService: IWindowsMainService + ) { + } + + public saveAndOpenWorkspace(window: CodeWindow, path: string): TPromise { + if (!window || !window.win || window.readyState !== ReadyState.READY || !window.openedWorkspace || !path || !this.isValidTargetWorkspacePath(window, path)) { + return TPromise.as(null); // return early if the window is not ready or disposed or does not have a workspace + } + + return this.doSaveAndOpenWorkspace(window, window.openedWorkspace, path); + } + + public createAndOpenWorkspace(window: CodeWindow, folders?: string[], path?: string): TPromise { + if (!window || !window.win || window.readyState !== ReadyState.READY || !this.isValidTargetWorkspacePath(window, path)) { + return TPromise.as(null); // return early if the window is not ready or disposed + } + + return this.workspacesService.createWorkspace(folders).then(workspace => { + return this.doSaveAndOpenWorkspace(window, workspace, path); + }); + } + + private isValidTargetWorkspacePath(window: CodeWindow, path?: string): boolean { + if (!path) { + return true; + } + + if (window.openedWorkspace && window.openedWorkspace.configPath === path) { + return false; // window is already opened on a workspace with that path + } + + // Prevent overwriting a workspace that is currently opened in another window + if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesService.getWorkspaceId(path), configPath: path })) { + const options: Electron.MessageBoxOptions = { + title: product.nameLong, + type: 'info', + buttons: [localize('ok', "OK")], + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + + const activeWindow = BrowserWindow.getFocusedWindow(); + if (activeWindow) { + dialog.showMessageBox(activeWindow, options); + } else { + dialog.showMessageBox(options); + } + + return false; + } + + return true; // OK + } + + private doSaveAndOpenWorkspace(window: CodeWindow, workspace: IWorkspaceIdentifier, path?: string): TPromise { + let savePromise: TPromise; + if (path) { + savePromise = this.workspacesService.saveWorkspace(workspace, path); + } else { + savePromise = TPromise.as(workspace); + } + + return savePromise.then(workspace => { + window.focus(); + + // Only open workspace when the window has not vetoed this + return this.lifecycleService.unload(window, UnloadReason.RELOAD, workspace).done(veto => { + if (!veto) { + + // Register window for backups and migrate current backups over + let backupPath: string; + if (window.config && !window.config.extensionDevelopmentPath) { + backupPath = this.backupService.registerWorkspaceBackupSync(workspace, window.config.backupPath); + } + + // Craft a new window configuration to use for the transition + const configuration: IWindowConfiguration = mixin({}, window.config); + configuration.folderPath = void 0; + configuration.workspace = workspace; + configuration.backupPath = backupPath; + + // Reload + window.reload(configuration); + } + }); + }); + } + + public openWorkspace(window = this.windowsMainService.getLastActiveWindow()): void { + let defaultPath: string; + if (window && window.openedWorkspace && !this.workspacesService.isUntitledWorkspace(window.openedWorkspace)) { + defaultPath = dirname(window.openedWorkspace.configPath); + } else { + defaultPath = this.getWorkspaceDialogDefaultPath(window ? (window.openedWorkspace || window.openedFolderPath) : void 0); + } + + this.windowsMainService.pickFileAndOpen({ + windowId: window ? window.id : void 0, + dialogOptions: { + buttonLabel: mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")), + title: localize('openWorkspaceTitle', "Open Workspace"), + filters: WORKSPACE_FILTER, + properties: ['openFile'], + defaultPath + } + }); + } + + public promptToSaveUntitledWorkspace(e: IWindowUnloadEvent, workspace: IWorkspaceIdentifier): void { + enum ConfirmResult { + SAVE, + DONT_SAVE, + CANCEL + } + + const save = { label: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), result: ConfirmResult.SAVE }; + const dontSave = { label: mnemonicButtonLabel(localize({ key: 'doNotSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save")), result: ConfirmResult.DONT_SAVE }; + const cancel = { label: localize('cancel', "Cancel"), result: ConfirmResult.CANCEL }; + + const buttons: { label: string; result: ConfirmResult; }[] = []; + if (isWindows) { + buttons.push(save, dontSave, cancel); + } else if (isLinux) { + buttons.push(dontSave, cancel, save); + } else { + buttons.push(save, cancel, dontSave); + } + + const options: Electron.MessageBoxOptions = { + title: this.environmentService.appNameLong, + message: localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"), + detail: localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."), + noLink: true, + type: 'warning', + buttons: buttons.map(button => button.label), + cancelId: buttons.indexOf(cancel) + }; + + if (isLinux) { + options.defaultId = 2; + } + + const res = dialog.showMessageBox(e.window.win, options); + + switch (buttons[res].result) { + + // Cancel: veto unload + case ConfirmResult.CANCEL: + e.veto(true); + break; + + // Don't Save: delete workspace + case ConfirmResult.DONT_SAVE: + this.workspacesService.deleteUntitledWorkspaceSync(workspace); + e.veto(false); + break; + + // Save: save workspace, but do not veto unload + case ConfirmResult.SAVE: { + const target = dialog.showSaveDialog(e.window.win, { + buttonLabel: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), + title: localize('saveWorkspace', "Save Workspace"), + filters: WORKSPACE_FILTER, + defaultPath: this.getWorkspaceDialogDefaultPath(workspace) + }); + + if (target) { + e.veto(this.workspacesService.saveWorkspace(workspace, target).then(() => false, () => false)); + } else { + e.veto(true); // keep veto if no target was provided + } + } + } + } + + private getWorkspaceDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { + let defaultPath: string; + if (workspace) { + if (isSingleFolderWorkspaceIdentifier(workspace)) { + defaultPath = dirname(workspace); + } else { + const resolvedWorkspace = this.workspacesService.resolveWorkspaceSync(workspace.configPath); + if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) { + defaultPath = dirname(resolvedWorkspace.folders[0].path); + } + } + } + + return defaultPath; + } } \ No newline at end of file diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index b8c5900a2d2..79cf5f2c105 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -308,7 +308,8 @@ export class View extends ViewEventHandler { } private getEditorClassName() { - return this._context.configuration.editor.editorClassName + ' ' + getThemeTypeSelector(this._context.theme.type); + let focused = this._textAreaHandler.isFocused() ? ' focused' : ''; + return this._context.configuration.editor.editorClassName + ' ' + getThemeTypeSelector(this._context.theme.type) + focused; } // --- begin event handlers @@ -323,7 +324,7 @@ export class View extends ViewEventHandler { return false; } public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { - this.domNode.toggleClassName('focused', e.isFocused); + this.domNode.setClassName(this.getEditorClassName()); if (e.isFocused) { this.outgoingEvents.emitViewFocusGained(); } else { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index fe646ba39e4..22efae7b65e 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -400,6 +400,7 @@ export abstract class CodeEditorWidget extends CommonCodeEditor implements edito this._view.render(false, true); this.hasView = true; + this._view.domNode.domNode.setAttribute('data-uri', model.uri.toString()); } } diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 94210b0572f..9ea43c880e6 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -88,14 +88,19 @@ export class BackupMainService implements IBackupMainService { } private moveBackupFolderSync(backupPath: string, moveFromPath: string): void { - if (!fs.existsSync(moveFromPath)) { - return; + + // Target exists: make sure to convert existing backups to empty window backups + if (fs.existsSync(backupPath)) { + this.convertToEmptyWindowBackup(backupPath); } - try { - fs.renameSync(moveFromPath, backupPath); - } catch (ex) { - this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`); + // When we have data to migrate from, move it over to the target location + if (fs.existsSync(moveFromPath)) { + try { + fs.renameSync(moveFromPath, backupPath); + } catch (ex) { + this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`); + } } } @@ -231,16 +236,7 @@ export class BackupMainService implements IBackupMainService { staleBackupWorkspaces.push({ workspaceIdentifier: workspaceId, backupPath, target: workspaceOrFolder.target }); if (missingWorkspace) { - const identifier = this.getRandomEmptyWindowId(); - this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces); - const newEmptyWindowBackupPath = path.join(this.backupHome, identifier); - try { - fs.renameSync(backupPath, newEmptyWindowBackupPath); - } catch (ex) { - this.logService.error(`Backup: Could not rename backup folder for missing workspace: ${ex.toString()}`); - - this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces); - } + this.convertToEmptyWindowBackup(backupPath); } } }); @@ -267,6 +263,27 @@ export class BackupMainService implements IBackupMainService { }); } + private convertToEmptyWindowBackup(backupPath: string): boolean { + + // New empty window backup + const identifier = this.getRandomEmptyWindowId(); + this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces); + + // Rename backupPath to new empty window backup path + const newEmptyWindowBackupPath = path.join(this.backupHome, identifier); + try { + fs.renameSync(backupPath, newEmptyWindowBackupPath); + } catch (ex) { + this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); + + this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces); + + return false; + } + + return true; + } + private hasBackupsSync(backupPath: string): boolean { try { const backupSchemas = extfs.readdirSync(backupPath); diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 063ebe91c6a..7d5a37d4edb 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -208,6 +208,33 @@ suite('BackupMainService', () => { assert.ok(fs.existsSync(path.join(workspaceBackupPath, 'backup.txt'))); assert.ok(!fs.existsSync(backupPathToMigrate)); + const emptyBackups = service.getEmptyWindowBackupPaths(); + assert.equal(0, emptyBackups.length); + + done(); + }); + + test('service backup migration makes sure to preserve existing backups', done => { + const backupPathToMigrate = service.toBackupPath(fooFile.fsPath); + fs.mkdirSync(backupPathToMigrate); + fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); + service.registerFolderBackupSync(backupPathToMigrate); + + const backupPathToPreserve = service.toBackupPath(barFile.fsPath); + fs.mkdirSync(backupPathToPreserve); + fs.writeFileSync(path.join(backupPathToPreserve, 'backup.txt'), 'Some Data'); + service.registerFolderBackupSync(backupPathToPreserve); + + const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate); + + assert.ok(fs.existsSync(workspaceBackupPath)); + assert.ok(fs.existsSync(path.join(workspaceBackupPath, 'backup.txt'))); + assert.ok(!fs.existsSync(backupPathToMigrate)); + + const emptyBackups = service.getEmptyWindowBackupPaths(); + assert.equal(1, emptyBackups.length); + assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0])).length); + done(); }); diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 36a1ed42492..01222188e6a 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -31,14 +31,7 @@ function getUniqueUserId(): string { return crypto.createHash('sha256').update(username).digest('hex').substr(0, 6); } -// Read this before there's any chance it is overwritten -// Related to https://github.com/Microsoft/vscode/issues/30624 -const xdgRuntimeDir = process.env['XDG_RUNTIME_DIR']; - function getNixIPCHandle(userDataPath: string, type: string): string { - if (xdgRuntimeDir) { - return path.join(xdgRuntimeDir, `${pkg.name}-${pkg.version}-${type}.sock`); - } return path.join(userDataPath, `${pkg.version}-${type}.sock`); } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4e8c9dab644..0c78c12054c 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -94,7 +94,7 @@ export interface IFileService { * * The optional parameter content can be used as value to fill into the new file. */ - createFile(resource: URI, content?: string): TPromise; + createFile(resource: URI, content?: string, options?: ICreateFileOptions): TPromise; /** * Creates a new folder with the given path. The returned promise @@ -523,6 +523,15 @@ export interface IResolveFileOptions { resolveSingleChildDescendants?: boolean; } +export interface ICreateFileOptions { + + /** + * Overwrite the file to create if it already exists on disk. Otherwise + * an error will be thrown (FILE_MODIFIED_SINCE). + */ + overwrite?: boolean; +} + export interface IImportResult { stat: IFileStat; isNew: boolean; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ac6500b1860..92310b76a73 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3869,6 +3869,21 @@ declare module 'vscode' { options?: ShellExecutionOptions; } + /** + * The scope of a task. + */ + export enum TaskScope { + /** + * The task is a global task + */ + Global = 1, + + /** + * The task is a workspace task + */ + Workspace = 2 + } + /** * A task to execute */ @@ -3877,8 +3892,10 @@ declare module 'vscode' { /** * Creates a new task. * + * @deprecated: Use the new constructors that allow specifying a target for the task. + * * @param definition The task definition as defined in the taskDefinitions extension point. - * @param name The task's name. Is presented in the user interface. + * @param scope The task's name. Is presented in the user interface. * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. * @param execution The process or shell execution. * @param problemMatchers the names of problem matchers to use, like '$tsc' @@ -3887,11 +3904,31 @@ declare module 'vscode' { */ constructor(taskDefinition: TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + /** + * Creates a new task. + * + * @param definition The task definition as defined in the taskDefinitions extension point. + * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. + * @param workspaceFolder The workspace folder this task is created for. + * @param name The task's name. Is presented in the user interface. + * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. + * @param execution The process or shell execution. + * @param problemMatchers the names of problem matchers to use, like '$tsc' + * or '$eslint'. Problem matchers can be contributed by an extension using + * the `problemMatchers` extension point. + */ + constructor(taskDefinition: TaskDefinition, target: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + /** * The task's definition. */ definition: TaskDefinition; + /** + * The task's scope. + */ + scope?: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder; + /** * The task's name */ diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index b1829b9387f..4c9ed5a511f 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -97,7 +97,7 @@ export function createApiFactory( const extHostQuickOpen = threadService.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(threadService)); const extHostTerminalService = threadService.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(threadService)); const extHostSCM = threadService.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(threadService, extHostCommands)); - const extHostTask = threadService.set(ExtHostContext.ExtHostTask, new ExtHostTask(threadService)); + const extHostTask = threadService.set(ExtHostContext.ExtHostTask, new ExtHostTask(threadService, extHostWorkspace)); const extHostCredentials = threadService.set(ExtHostContext.ExtHostCredentials, new ExtHostCredentials(threadService)); const extHostWindow = threadService.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(threadService)); threadService.set(ExtHostContext.ExtHostExtensionService, extensionService); @@ -599,6 +599,7 @@ export function createApiFactory( TaskGroup: extHostTypes.TaskGroup, ProcessExecution: extHostTypes.ProcessExecution, ShellExecution: extHostTypes.ShellExecution, + TaskScope: extHostTypes.TaskScope, Task: extHostTypes.Task, ConfigurationTarget: extHostTypes.ConfigurationTarget }; diff --git a/src/vs/workbench/api/node/extHostFileSystemEventService.ts b/src/vs/workbench/api/node/extHostFileSystemEventService.ts index 822298414c3..6038446ffc4 100644 --- a/src/vs/workbench/api/node/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/node/extHostFileSystemEventService.ts @@ -6,7 +6,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import { Disposable } from './extHostTypes'; -import { match } from 'vs/base/common/glob'; +import { parse } from 'vs/base/common/glob'; import { Uri, FileSystemWatcher as _FileSystemWatcher } from 'vscode'; import { FileSystemEvents, ExtHostFileSystemEventServiceShape } from './extHost.protocol'; @@ -43,24 +43,26 @@ class FileSystemWatcher implements _FileSystemWatcher { this._config += 0b100; } + const parsedPattern = parse(globPattern); + let subscription = dispatcher(events => { if (!ignoreCreateEvents) { for (let created of events.created) { - if (match(globPattern, created.fsPath)) { + if (parsedPattern(created.fsPath)) { this._onDidCreate.fire(created); } } } if (!ignoreChangeEvents) { for (let changed of events.changed) { - if (match(globPattern, changed.fsPath)) { + if (parsedPattern(changed.fsPath)) { this._onDidChange.fire(changed); } } } if (!ignoreDeleteEvents) { for (let deleted of events.deleted) { - if (match(globPattern, deleted.fsPath)) { + if (parsedPattern(deleted.fsPath)) { this._onDidDelete.fire(deleted); } } diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index a174c82f1b7..b644dfb2424 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import URI from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import * as Objects from 'vs/base/common/objects'; @@ -15,6 +16,7 @@ import * as TaskSystem from 'vs/workbench/parts/tasks/common/tasks'; import { MainContext, MainThreadTaskShape, ExtHostTaskShape, IMainContext } from 'vs/workbench/api/node/extHost.protocol'; import * as types from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; interface StringMap { @@ -295,13 +297,13 @@ namespace ShellConfiguration { namespace Tasks { - export function from(tasks: vscode.Task[], extension: IExtensionDescription): TaskSystem.Task[] { + export function from(tasks: vscode.Task[], rootFolder: vscode.WorkspaceFolder, extension: IExtensionDescription): TaskSystem.Task[] { if (tasks === void 0 || tasks === null) { return []; } let result: TaskSystem.Task[] = []; for (let task of tasks) { - let converted = fromSingle(task, extension); + let converted = fromSingle(task, rootFolder, extension); if (converted) { result.push(converted); } @@ -309,7 +311,7 @@ namespace Tasks { return result; } - function fromSingle(task: vscode.Task, extension: IExtensionDescription): TaskSystem.ContributedTask { + function fromSingle(task: vscode.Task, rootFolder: vscode.WorkspaceFolder, extension: IExtensionDescription): TaskSystem.ContributedTask { if (typeof task.name !== 'string') { return undefined; } @@ -326,10 +328,27 @@ namespace Tasks { return undefined; } command.presentation = PresentationOptions.from(task.presentationOptions); - let source = { + + let taskScope: types.TaskScope.Global | types.TaskScope.Workspace | vscode.WorkspaceFolder | undefined = task.scope; + let workspaceFolder: vscode.WorkspaceFolder | undefined; + let scope: TaskSystem.TaskScope; + // For backwards compatibility + if (taskScope === void 0) { + scope = TaskSystem.TaskScope.Folder; + workspaceFolder = rootFolder; + } else if (taskScope === types.TaskScope.Global) { + scope = TaskSystem.TaskScope.Global; + } else if (taskScope === types.TaskScope.Workspace) { + scope = TaskSystem.TaskScope.Workspace; + } else { + workspaceFolder = taskScope; + } + let source: TaskSystem.ExtensionTaskSource = { kind: TaskSystem.TaskSourceKind.Extension, label: typeof task.source === 'string' ? task.source : extension.name, - extension: extension.id + extension: extension.id, + scope: scope, + workspaceFolder: workspaceFolder ? { uri: workspaceFolder.uri as URI } : undefined }; let label = nls.localize('task.label', '{0}: {1}', source.label, task.name); let key = (task as types.Task).definitionKey; @@ -398,11 +417,13 @@ interface HandlerData { export class ExtHostTask implements ExtHostTaskShape { private _proxy: MainThreadTaskShape; + private _extHostWorkspace: ExtHostWorkspace; private _handleCounter: number; private _handlers: Map; - constructor(mainContext: IMainContext) { + constructor(mainContext: IMainContext, extHostWorkspace: ExtHostWorkspace) { this._proxy = mainContext.get(MainContext.MainThreadTask); + this._extHostWorkspace = extHostWorkspace; this._handleCounter = 0; this._handlers = new Map(); }; @@ -426,8 +447,9 @@ export class ExtHostTask implements ExtHostTaskShape { return TPromise.wrapError(new Error('no handler found')); } return asWinJsPromise(token => handler.provider.provideTasks(token)).then(value => { + let workspaceFolders = this._extHostWorkspace.getWorkspaceFolders(); return { - tasks: Tasks.from(value, handler.extension), + tasks: Tasks.from(value, workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0] : undefined, handler.extension), extension: handler.extension }; }); diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index e1b60f1ba51..a9b213648b8 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1214,10 +1214,16 @@ export class ShellExecution implements vscode.ShellExecution { } } +export enum TaskScope { + Global = 1, + Workspace = 2 +} + export class Task implements vscode.Task { private _definition: vscode.TaskDefinition; private _definitionKey: string; + private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder; private _name: string; private _execution: ProcessExecution | ShellExecution; private _problemMatchers: string[]; @@ -1227,11 +1233,29 @@ export class Task implements vscode.Task { private _group: TaskGroup; private _presentationOptions: vscode.TaskPresentationOptions; - constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]) { + constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + constructor(definition: vscode.TaskDefinition, arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, arg3: any, arg4?: any, arg5?: any, arg6?: any) { this.definition = definition; - this.name = name; - this.source = source; - this.execution = execution; + let problemMatchers: string | string[]; + if (typeof arg2 === 'string') { + this.name = arg2; + this.source = arg3; + this.execution = arg4; + problemMatchers = arg5; + } else if (arg2 === TaskScope.Global || arg2 === TaskScope.Workspace) { + this.target = arg2; + this.name = arg3; + this.source = arg4; + this.execution = arg5; + problemMatchers = arg6; + } else { + this.target = arg2; + this.name = arg3; + this.source = arg4; + this.execution = arg5; + problemMatchers = arg6; + } if (typeof problemMatchers === 'string') { this._problemMatchers = [problemMatchers]; this._hasDefinedMatchers = true; @@ -1266,6 +1290,14 @@ export class Task implements vscode.Task { return this._definitionKey; } + get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder { + return this._scope; + } + + set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { + this._scope = value; + } + get name(): string { return this._name; } diff --git a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts index ad96768d0d5..ccf2795916f 100644 --- a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts +++ b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts @@ -32,13 +32,21 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC public provideTextContent(resource: uri): TPromise { let process: IProcess; + let sourceRef: number; + if (resource.query) { const keyvalues = resource.query.split('&'); for (let keyvalue of keyvalues) { const pair = keyvalue.split('='); - if (pair.length === 2 && pair[0] === 'session') { - process = this.debugService.findProcessByUUID(decodeURIComponent(pair[1])); - break; + if (pair.length === 2) { + switch (pair[0]) { + case 'session': + process = this.debugService.findProcessByUUID(decodeURIComponent(pair[1])); + break; + case 'sourceRef': + sourceRef = parseInt(pair[1]); + break; + } } } } @@ -55,18 +63,26 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC let rawSource: DebugProtocol.Source; if (source) { rawSource = source.raw; + if (!sourceRef) { + sourceRef = source.reference; + } } else { - // Remove debug: scheme - rawSource = { path: resource.with({ scheme: '', query: '' }).toString(true) }; + // create a Source + rawSource = { + path: resource.with({ scheme: '', query: '' }).toString(true), // Remove debug: scheme + sourceReference: sourceRef + }; } - return process.session.source({ sourceReference: source ? source.reference : undefined, source: rawSource }).then(response => { + return process.session.source({ sourceReference: sourceRef, source: rawSource }).then(response => { + const mime = response.body.mimeType || guessMimeTypes(resource.toString())[0]; const modePromise = this.modeService.getOrCreateMode(mime); const model = this.modelService.createModel(response.body.content, modePromise, resource); return model; }, (err: DebugProtocol.ErrorResponse) => { + this.debugService.sourceIsNotAvailable(resource); const modePromise = this.modeService.getOrCreateMode(MIME_TEXT); const model = this.modelService.createModel(err.message, modePromise, resource); diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index a97e3d17f85..d0ff9fd24e1 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -56,10 +56,6 @@ export class DebugViewlet extends PersistentViewsViewlet { public focus(): void { super.focus(); - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.views[0].focusBody(); - } - if (this.startDebugActionItem) { this.startDebugActionItem.focus(); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index cedea9bb663..6761e846ab6 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -26,7 +26,7 @@ import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/parts/e import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import * as cp from 'child_process'; +import * as fs from 'fs'; import { distinct } from 'vs/base/common/arrays'; interface IExtensionsContent { @@ -68,7 +68,6 @@ export class ExtensionTipsService implements IExtensionTipsService { this._suggestTips(); this._suggestWorkspaceRecommendations(); - this._suggestBasedOnExecutables(); } getWorkspaceRecommendations(): TPromise { @@ -92,7 +91,7 @@ export class ExtensionTipsService implements IExtensionTipsService { const fileBased = Object.keys(this._fileBasedRecommendations) .filter(recommendation => allRecomendations.indexOf(recommendation) !== -1); - const exeBased = distinct(this._exeBasedRecommendations); + const exeBased = distinct(this._suggestBasedOnExecutables()); this.telemetryService.publicLog('extensionRecommendations:unfiltered', { fileBased, exeBased }); @@ -319,15 +318,42 @@ export class ExtensionTipsService implements IExtensionTipsService { }); } - private _suggestBasedOnExecutables() { - const cmd = process.platform === 'win32' ? 'where' : 'which'; + private _suggestBasedOnExecutables(): string[] { + if (!process.env.PATH || this._exeBasedRecommendations.length > 0) { + return this._exeBasedRecommendations; + } + + let envpaths = process.env.PATH.split(process.platform === 'win32' ? ';' : ':'); + let foundExecutables: Set = new Set(); + + // Loop through recommended extensions forEach(product.exeBasedExtensionTips, entry => { - cp.exec(`${cmd} ${entry.value.replace(/,/g, ' ')}`, (err, stdout, stderr) => { - if (stdout) { - this._exeBasedRecommendations.push(entry.key); + let executables = entry.value.split(','); + + // Loop through executables that would result in recommending current extension + for (let i = 0; i < executables.length; i++) { + if (!foundExecutables.has(executables[i])) { + + // Loop through paths in PATH to find current executable + for (let pathEntry of envpaths) { + let fullPath = paths.join(pathEntry, executables[i]); + if (process.platform === 'win32') { + fullPath += '.exe'; + } + if (fs.existsSync(fullPath)) { + foundExecutables.add(executables[i]); + break; + } + } } - }); + if (foundExecutables.has(executables[i])) { + this._exeBasedRecommendations.push(entry.key); + break; + } + } }); + + return this._exeBasedRecommendations; } private setIgnoreRecommendationsConfig(configVal: boolean) { diff --git a/src/vs/workbench/parts/tasks/browser/quickOpen.ts b/src/vs/workbench/parts/tasks/browser/quickOpen.ts index 2f64e812655..32a4d495b83 100644 --- a/src/vs/workbench/parts/tasks/browser/quickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/quickOpen.ts @@ -21,7 +21,7 @@ import { ActionBarContributor, ContributableActionProvider } from 'vs/workbench/ export class TaskEntry extends Model.QuickOpenEntry { - constructor(protected taskService: ITaskService, protected quickOpenService: IQuickOpenService, protected _task: CustomTask | ContributedTask, highlights: Model.IHighlight[] = []) { + constructor(protected quickOpenService: IQuickOpenService, protected taskService: ITaskService, protected _task: CustomTask | ContributedTask, highlights: Model.IHighlight[] = []) { super(highlights); } @@ -29,6 +29,17 @@ export class TaskEntry extends Model.QuickOpenEntry { return this.task._label; } + public getDescription(): string { + if (!this.taskService.hasMultipleFolders()) { + return null; + } + let workspaceFolder = Task.getWorkspaceFolder(this.task); + if (!workspaceFolder) { + return null; + } + return workspaceFolder.uri.fsPath; + } + public getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, tasks", this.getLabel()); } diff --git a/src/vs/workbench/parts/tasks/browser/taskQuickOpen.ts b/src/vs/workbench/parts/tasks/browser/taskQuickOpen.ts index 46d3c023200..b7325389b2b 100644 --- a/src/vs/workbench/parts/tasks/browser/taskQuickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/taskQuickOpen.ts @@ -14,12 +14,11 @@ import { CustomTask, ContributedTask } from 'vs/workbench/parts/tasks/common/tas import { ITaskService } from 'vs/workbench/parts/tasks/common/taskService'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; - import * as base from './quickOpen'; class TaskEntry extends base.TaskEntry { - constructor(taskService: ITaskService, quickOpenService: IQuickOpenService, task: CustomTask | ContributedTask, highlights: Model.IHighlight[] = []) { - super(taskService, quickOpenService, task, highlights); + constructor(quickOpenService: IQuickOpenService, taskService: ITaskService, task: CustomTask | ContributedTask, highlights: Model.IHighlight[] = []) { + super(quickOpenService, taskService, task, highlights); } public run(mode: QuickOpen.Mode, context: Model.IContext): boolean { @@ -36,8 +35,8 @@ export class QuickOpenHandler extends base.QuickOpenHandler { constructor( @IQuickOpenService quickOpenService: IQuickOpenService, - @ITaskService taskService: ITaskService, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @ITaskService taskService: ITaskService ) { super(quickOpenService, taskService); this.activationPromise = extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask'); @@ -54,7 +53,7 @@ export class QuickOpenHandler extends base.QuickOpenHandler { } protected createEntry(task: CustomTask | ContributedTask, highlights: Model.IHighlight[]): base.TaskEntry { - return new TaskEntry(this.taskService, this.quickOpenService, task, highlights); + return new TaskEntry(this.quickOpenService, this.taskService, task, highlights); } public getEmptyLabel(searchString: string): string { diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index 7598b170398..d56ad59ce58 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -59,7 +59,8 @@ export interface ITaskService extends IEventEmitter { getTasksForGroup(group: string): TPromise; getRecentlyUsedTasks(): LinkedMap; - canCustomize(): boolean; + hasMultipleFolders(); + canCustomize(task: ContributedTask | CustomTask): boolean; customize(task: ContributedTask | CustomTask, properties?: {}, openConfig?: boolean): TPromise; openConfig(task: CustomTask): TPromise; diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index fd9b28ce2d5..9c15a51436b 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import URI from 'vs/base/common/uri'; import * as Types from 'vs/base/common/types'; import { IJSONSchemaMap } from 'vs/base/common/jsonSchema'; @@ -215,34 +216,47 @@ export namespace TaskGroup { export type TaskGroup = 'clean' | 'build' | 'rebuild' | 'test'; +export enum TaskScope { + Global = 1, + Workspace = 2, + Folder = 3 +} + export namespace TaskSourceKind { export const Workspace: 'workspace' = 'workspace'; export const Extension: 'extension' = 'extension'; export const Composite: 'composite' = 'composite'; } +export interface WorkspaceFolder { + uri: URI; +} + export interface TaskSourceConfigElement { + workspaceFolder: WorkspaceFolder; file: string; index: number; element: any; } export interface WorkspaceTaskSource { - kind: 'workspace'; - label: string; - config: TaskSourceConfigElement; - customizes?: TaskIdentifier; + readonly kind: 'workspace'; + readonly label: string; + readonly config: TaskSourceConfigElement; + readonly customizes?: TaskIdentifier; } export interface ExtensionTaskSource { - kind: 'extension'; - label: string; - extension: string; + readonly kind: 'extension'; + readonly label: string; + readonly extension: string; + readonly scope: TaskScope; + readonly workspaceFolder: WorkspaceFolder | undefined; } export interface CompositeTaskSource { - kind: 'composite'; - label: string; + readonly kind: 'composite'; + readonly label: string; } export type TaskSource = WorkspaceTaskSource | ExtensionTaskSource | CompositeTaskSource; @@ -411,6 +425,16 @@ export namespace Task { } } + export function getWorkspaceFolder(task: Task): WorkspaceFolder | undefined { + if (CustomTask.is(task)) { + return task._source.config.workspaceFolder; + } else if (ContributedTask.is(task)) { + return task._source.workspaceFolder; + } else { + return undefined; + } + } + export function getTelemetryKind(task: Task): string { if (ContributedTask.is(task)) { return 'extension'; diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index 5d30f4a319b..c02e59693d7 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -76,7 +76,7 @@ import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; import { ITaskSystem, ITaskResolver, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskSystemEvents, TaskTerminateResponse } from 'vs/workbench/parts/tasks/common/taskSystem'; -import { Task, CustomTask, ConfiguringTask, ContributedTask, CompositeTask, TaskSet, TaskGroup, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; +import { Task, CustomTask, ConfiguringTask, ContributedTask, CompositeTask, TaskSet, TaskGroup, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, TaskIdentifier, WorkspaceFolder } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, TaskServiceEvents, ITaskProvider, TaskEvent, RunOptions, CustomizationProperties } from 'vs/workbench/parts/tasks/common/taskService'; import { templates as taskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; @@ -618,7 +618,12 @@ interface WorkspaceTaskResult { hasErrors: boolean; } -interface WorkspaceConfigurationResult { +interface WorkspaceFolderTaskResult extends WorkspaceTaskResult { + workspaceFolder: WorkspaceFolder; +} + +interface WorkspaceFolderConfigurationResult { + workspaceFolder: WorkspaceFolder; config: TaskConfig.ExternalTaskRunnerConfiguration; hasErrors: boolean; } @@ -657,9 +662,12 @@ class TaskService extends EventEmitter implements ITaskService { private quickOpenService: IQuickOpenService; private _configHasErrors: boolean; + private _schemaVersion: JsonSchemaVersion; + private _executionEngine: ExecutionEngine; + private _workspaceFolders: WorkspaceFolder[]; private _providers: Map; - private _workspaceTasksPromise: TPromise; + private _workspaceTasksPromise: TPromise>; private _taskSystem: ITaskSystem; private _taskSystemListeners: IDisposable[]; @@ -711,22 +719,14 @@ class TaskService extends EventEmitter implements ITaskService { if (!this._taskSystem && !this._workspaceTasksPromise) { return; } - this.updateWorkspaceTasks(); - if (!this._taskSystem) { - return; - } - let currentExecutionEngine = this._taskSystem instanceof TerminalTaskSystem - ? ExecutionEngine.Terminal - : this._taskSystem instanceof ProcessTaskSystem - ? ExecutionEngine.Process - : ExecutionEngine._default; - if (currentExecutionEngine !== this.getExecutionEngine()) { + let folderSetup = this.computeWorkspaceFolders(); + if (this._executionEngine !== folderSetup[1] && this._taskSystem && this._taskSystem.getActiveTasks().length > 0) { this.messageService.show( Severity.Info, { message: nls.localize( 'TaskSystem.noHotSwap', - 'Changing the task execution engine requires to reload the Window' + 'Changing the task execution engine with an active task running requires to reload the Window' ), actions: [ new ReloadWindowAction(ReloadWindowAction.ID, ReloadWindowAction.LABEL, this._windowServive), @@ -734,8 +734,17 @@ class TaskService extends EventEmitter implements ITaskService { ] } ); + return; } + this._workspaceFolders = folderSetup[0]; + this._executionEngine = folderSetup[1]; + this._schemaVersion = folderSetup[2]; + this.updateWorkspaceTasks(); }); + let folderSetup = this.computeWorkspaceFolders(); + this._workspaceFolders = folderSetup[0]; + this._executionEngine = folderSetup[1]; + this._schemaVersion = folderSetup[2]; lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown())); this.registerCommands(); } @@ -814,20 +823,14 @@ class TaskService extends EventEmitter implements ITaskService { } public getTask(identifier: string): TPromise { - return this.getTaskSets().then((sets) => { - let resolver = this.createResolver(sets); + return this.getAllTasks().then((tasks) => { + let resolver = this.createResolver(tasks); return resolver.resolve(identifier); }); } public tasks(): TPromise { - return this.getTaskSets().then((sets) => { - let result: Task[] = []; - for (let set of sets) { - result.push(...set.tasks); - } - return result; - }); + return this.getAllTasks(); }; public isActive(): TPromise { @@ -881,10 +884,10 @@ class TaskService extends EventEmitter implements ITaskService { } public build(): TPromise { - return this.getTaskSets().then((values) => { - let runnable = this.createRunnableTask(values, TaskGroup.Build); + return this.getAllTasks().then((tasks) => { + let runnable = this.createRunnableTask(tasks, TaskGroup.Build); if (!runnable || !runnable.task) { - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V0_1_0) { + if (this._schemaVersion === JsonSchemaVersion.V0_1_0) { throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask1', 'No build task defined. Mark a task with \'isBuildCommand\' in the tasks.json file.'), TaskErrors.NoBuildTask); } else { throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask2', 'No build task defined. Mark a task with as a \'build\' group in the tasks.json file.'), TaskErrors.NoBuildTask); @@ -906,10 +909,10 @@ class TaskService extends EventEmitter implements ITaskService { } public runTest(): TPromise { - return this.getTaskSets().then((values) => { - let runnable = this.createRunnableTask(values, TaskGroup.Test); + return this.getAllTasks().then((tasks) => { + let runnable = this.createRunnableTask(tasks, TaskGroup.Test); if (!runnable || !runnable.task) { - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V0_1_0) { + if (this._schemaVersion === JsonSchemaVersion.V0_1_0) { throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask1', 'No test task defined. Mark a task with \'isTestCommand\' in the tasks.json file.'), TaskErrors.NoTestTask); } else { throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask2', 'No test task defined. Mark a task with as a \'test\' group in the tasks.json file.'), TaskErrors.NoTestTask); @@ -923,8 +926,8 @@ class TaskService extends EventEmitter implements ITaskService { } public run(task: string | Task, options?: RunOptions): TPromise { - return this.getTaskSets().then((values) => { - let resolver = this.createResolver(values); + return this.getAllTasks().then((tasks) => { + let resolver = this.createResolver(tasks); let requested: string; let toExecute: Task; if (Types.isString(task)) { @@ -955,7 +958,7 @@ class TaskService extends EventEmitter implements ITaskService { } private shouldAttachProblemMatcher(task: Task): boolean { - if (!this.canCustomize()) { + if (!this.canCustomize(task)) { return false; } if (task.group !== void 0 && task.group !== TaskGroup.Build) { @@ -1033,25 +1036,40 @@ class TaskService extends EventEmitter implements ITaskService { } public getTasksForGroup(group: string): TPromise { - return this.getTaskSets().then((values) => { + return this.getAllTasks().then((tasks) => { let result: Task[] = []; - for (let value of values) { - for (let task of value.tasks) { - if (task.group === group) { - result.push(task); - } + for (let task of tasks) { + if (task.group === group) { + result.push(task); } } return result; }); } - public canCustomize(): boolean { - return this.getJsonSchemaVersion() === JsonSchemaVersion.V2_0_0; + public hasMultipleFolders(): boolean { + return this._workspaceFolders && this._workspaceFolders.length > 1; + } + + public canCustomize(task: Task): boolean { + if (this._schemaVersion !== JsonSchemaVersion.V2_0_0) { + return false; + } + if (CustomTask.is(task)) { + return true; + } + if (ContributedTask.is(task)) { + return !!Task.getWorkspaceFolder(task); + } + return false; } public customize(task: ContributedTask | CustomTask, properties?: CustomizationProperties, openConfig?: boolean): TPromise { - let configuration = this.getConfiguration(); + let workspaceFolder = Task.getWorkspaceFolder(task); + if (!workspaceFolder) { + return TPromise.as(undefined); + } + let configuration = this.getConfiguration(workspaceFolder); if (configuration.hasParseErrors) { this.messageService.show(Severity.Warning, nls.localize('customizeParseErrors', 'The current task configuration has errors. Please fix the errors first before customizing a task.')); return TPromise.as(undefined); @@ -1114,12 +1132,12 @@ class TaskService extends EventEmitter implements ITaskService { fileConfig.problemMatcher = properties.problemMatcher; value.key = 'tasks.problemMatchers'; value.value = fileConfig.problemMatcher; - promise = this.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, value); + promise = this.writeConfiguration(workspaceFolder, value); } else if (properties.group !== void 0) { fileConfig.group = properties.group; value.key = 'tasks.group'; value.value = fileConfig.group; - promise = this.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, value); + promise = this.writeConfiguration(workspaceFolder, value); } } else { if (!Array.isArray(fileConfig.tasks)) { @@ -1132,7 +1150,7 @@ class TaskService extends EventEmitter implements ITaskService { } else { fileConfig.tasks[index] = toCustomize; } - promise = this.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, value); + promise = this.writeConfiguration(workspaceFolder, value); } }; if (!promise) { @@ -1156,7 +1174,18 @@ class TaskService extends EventEmitter implements ITaskService { }); } + private writeConfiguration(workspaceFolder: WorkspaceFolder, value: IConfigurationValue): TPromise { + if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + return this.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, value); + } else if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + return this.configurationEditingService.writeConfiguration(ConfigurationTarget.FOLDER, value, { scopes: { resource: workspaceFolder.uri } }); + } else { + return undefined; + } + } + public openConfig(task: CustomTask): TPromise { + // @ToDo need to adopt since this is not working anymore let resource = this.contextService.toResource(task._source.config.file); return this.editorService.openEditor({ resource: resource, @@ -1167,26 +1196,24 @@ class TaskService extends EventEmitter implements ITaskService { }, false).then(() => undefined); } - private createRunnableTask(sets: TaskSet[], group: TaskGroup): { task: Task; resolver: ITaskResolver } { + private createRunnableTask(tasks: Task[], group: TaskGroup): { task: Task; resolver: ITaskResolver } { let idMap: IStringDictionary = Object.create(null); let labelMap: IStringDictionary = Object.create(null); let identifierMap: IStringDictionary = Object.create(null); let workspaceTasks: Task[] = []; let extensionTasks: Task[] = []; - sets.forEach((set) => { - set.tasks.forEach((task) => { - idMap[task._id] = task; - labelMap[task._label] = task; - identifierMap[task.identifier] = task; - if (group && task.group === group) { - if (task._source.kind === TaskSourceKind.Workspace) { - workspaceTasks.push(task); - } else { - extensionTasks.push(task); - } + tasks.forEach((task) => { + idMap[task._id] = task; + labelMap[task._label] = task; + identifierMap[task.identifier] = task; + if (group && task.group === group) { + if (task._source.kind === TaskSourceKind.Workspace) { + workspaceTasks.push(task); + } else { + extensionTasks.push(task); } - }); + } }); let resolver: ITaskResolver = { resolve: (id: string) => { @@ -1222,15 +1249,13 @@ class TaskService extends EventEmitter implements ITaskService { } } - private createResolver(sets: TaskSet[]): ITaskResolver { + private createResolver(tasks: Task[]): ITaskResolver { let labelMap: IStringDictionary = Object.create(null); let identifierMap: IStringDictionary = Object.create(null); - sets.forEach((set) => { - set.tasks.forEach((task) => { - labelMap[task._label] = task; - identifierMap[task.identifier] = task; - }); + tasks.forEach((task) => { + labelMap[task._label] = task; + identifierMap[task.identifier] = task; }); return { resolve: (id: string) => { @@ -1299,8 +1324,7 @@ class TaskService extends EventEmitter implements ITaskService { if (this._taskSystem) { return this._taskSystem; } - let engine = this.getExecutionEngine(); - if (engine === ExecutionEngine.Terminal) { + if (this._executionEngine === ExecutionEngine.Terminal) { this._taskSystem = new TerminalTaskSystem( this.terminalService, this.outputService, this.markerService, this.modelService, this.configurationResolverService, this.telemetryService, @@ -1322,7 +1346,7 @@ class TaskService extends EventEmitter implements ITaskService { return this._taskSystem; } - private getTaskSets(): TPromise { + private getAllTasks(): TPromise { return this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask').then(() => { return new TPromise((resolve, reject) => { let result: TaskSet[] = []; @@ -1340,7 +1364,7 @@ class TaskService extends EventEmitter implements ITaskService { resolve(result); } }; - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V2_0_0 && this._providers.size > 0) { + if (this._schemaVersion === JsonSchemaVersion.V2_0_0 && this._providers.size > 0) { this._providers.forEach((provider) => { counter++; provider.provideTasks().done(done, error); @@ -1349,57 +1373,95 @@ class TaskService extends EventEmitter implements ITaskService { resolve(result); } }); - }).then((result) => { - return this.getWorkspaceTasks().then((workspaceTaskResult) => { - let workspaceTasksToDelete: Task[] = []; - let configurations = workspaceTaskResult.configurations; - let legacyTaskConfigurations = workspaceTaskResult.set ? this.getLegacyTaskConfigurations(workspaceTaskResult.set) : undefined; - if (configurations || legacyTaskConfigurations) { - for (let set of result) { - for (let i = 0; i < set.tasks.length; i++) { - let task = set.tasks[i]; - if (!ContributedTask.is(task)) { - continue; - } - if (configurations) { - let configuringTask = configurations.byIdentifier[task.defines._key]; - if (configuringTask) { - set.tasks[i] = TaskConfig.createCustomTask(task, configuringTask); + }).then((contributedTaskSets) => { + let result: Task[] = []; + let contributedTasks: Map = new Map(); + for (let set of contributedTaskSets) { + for (let task of set.tasks) { + if (!ContributedTask.is(task)) { + continue; + } + let workspaceFolder = task._source.workspaceFolder; + if (workspaceFolder) { + let values = contributedTasks.get(workspaceFolder.uri.toString()); + if (!values) { + values = [task]; + contributedTasks.set(workspaceFolder.uri.toString(), values); + } else { + values.push(task); + } + } else { + result.push(task); + } + } + } + return this.getWorkspaceTasks().then((customTasks) => { + customTasks.forEach((folderTasks, key) => { + let contributed = contributedTasks.get(key); + if (!folderTasks.set) { + if (contributed) { + result.push(...contributed); + } + return; + } + + if (!contributed) { + result.push(...folderTasks.set.tasks); + } else { + let configurations = folderTasks.configurations; + let legacyTaskConfigurations = folderTasks.set ? this.getLegacyTaskConfigurations(folderTasks.set) : undefined; + let customTasksToDelete: Task[] = []; + if (configurations || legacyTaskConfigurations) { + for (let task of contributed) { + if (!ContributedTask.is(task)) { continue; } - } - if (legacyTaskConfigurations) { - let configuringTask = legacyTaskConfigurations[task.defines._key]; - if (configuringTask) { - set.tasks[i] = TaskConfig.createCustomTask(task, configuringTask); - workspaceTasksToDelete.push(configuringTask); - set.tasks[i] = configuringTask; - continue; + if (configurations) { + let configuringTask = configurations.byIdentifier[task.defines._key]; + if (configuringTask) { + result.push(TaskConfig.createCustomTask(task, configuringTask)); + } else { + result.push(task); + } + } else if (legacyTaskConfigurations) { + let configuringTask = legacyTaskConfigurations[task.defines._key]; + if (configuringTask) { + result.push(TaskConfig.createCustomTask(task, configuringTask)); + customTasksToDelete.push(configuringTask); + } else { + result.push(task); + } + } else { + result.push(task); } } + if (customTasksToDelete.length > 0) { + let toDelete = customTasksToDelete.reduce>((map, task) => { + map[task._id] = true; + return map; + }, Object.create(null)); + for (let task of folderTasks.set.tasks) { + if (toDelete[task._id]) { + continue; + } + result.push(task); + } + } else { + result.push(...folderTasks.set.tasks); + } + } else { + result.push(...folderTasks.set.tasks); + result.push(...contributed); } } - } - if (workspaceTaskResult.set) { - if (workspaceTasksToDelete.length > 0) { - let tasks = workspaceTaskResult.set.tasks; - let newSet: TaskSet = { - extension: workspaceTaskResult.set.extension, - tasks: [] - }; - let toDelete = workspaceTasksToDelete.reduce>((map, task) => { - map[task._id] = true; - return map; - }, Object.create(null)); - newSet.tasks = tasks.filter(task => !toDelete[task._id]); - result.push(newSet); - } else { - result.push(workspaceTaskResult.set); - } - } + }); return result; }, () => { // If we can't read the tasks.json file provide at least the contributed tasks + let result: Task[] = []; + for (let set of contributedTaskSets) { + result.push(...set.tasks); + } return result; }); }); @@ -1431,7 +1493,7 @@ class TaskService extends EventEmitter implements ITaskService { return result; } - private getWorkspaceTasks(): TPromise { + private getWorkspaceTasks(): TPromise> { if (this._workspaceTasksPromise) { return this._workspaceTasksPromise; } @@ -1441,113 +1503,162 @@ class TaskService extends EventEmitter implements ITaskService { private updateWorkspaceTasks(): void { this._workspaceTasksPromise = this.computeWorkspaceTasks().then(value => { - this._configHasErrors = value.hasErrors; - if (this._taskSystem instanceof ProcessTaskSystem) { - this._taskSystem.hasErrors(this._configHasErrors); + if (this._executionEngine === ExecutionEngine.Process && this._taskSystem instanceof ProcessTaskSystem) { + // We can only have a process engine if we have one folder. + value.forEach((value) => { + this._configHasErrors = value.hasErrors; + (this._taskSystem as ProcessTaskSystem).hasErrors(this._configHasErrors); + }); } return value; }); } - private computeWorkspaceTasks(): TPromise { - let configPromise: TPromise; - { - let { config, hasParseErrors } = this.getConfiguration(); - if (hasParseErrors) { - return TPromise.as({ set: undefined, hasErrors: true, configurations: undefined }); + private computeWorkspaceTasks(): TPromise> { + if (this._workspaceFolders.length === 0) { + return TPromise.as(new Map()); + } else { + let promises: TPromise[] = []; + for (let folder of this._workspaceFolders) { + promises.push(this.computeWorkspaceFolderTasks(folder).then((value) => value, () => undefined)); } - let engine = ExecutionEngine._default; - if (config) { - engine = TaskConfig.ExecutionEngine.from(config); - if (engine === ExecutionEngine.Process) { - if (this.hasDetectorSupport(config)) { - configPromise = new ProcessRunnerDetector(this.fileService, this.contextService, this.configurationResolverService, config).detect(true).then((value): WorkspaceConfigurationResult => { - let hasErrors = this.printStderr(value.stderr); - let detectedConfig = value.config; - if (!detectedConfig) { - return { config, hasErrors }; - } - let result: TaskConfig.ExternalTaskRunnerConfiguration = Objects.clone(config); - let configuredTasks: IStringDictionary = Object.create(null); - if (!result.tasks) { - if (detectedConfig.tasks) { - result.tasks = detectedConfig.tasks; - } - } else { - result.tasks.forEach(task => configuredTasks[task.taskName] = task); - detectedConfig.tasks.forEach((task) => { - if (!configuredTasks[task.taskName]) { - result.tasks.push(task); - } - }); - } - return { config: result, hasErrors }; - }); - } else { - configPromise = TPromise.as({ config, hasErrors: false }); + return TPromise.join(promises).then((values) => { + let result = new Map(); + for (let value of values) { + if (value) { + result.set(value.workspaceFolder.uri.toString(), value); } - } else { - configPromise = TPromise.as({ config, hasErrors: false }); } + return result; + }); + } + } + + private computeWorkspaceFolderTasks(workspaceFolder: WorkspaceFolder): TPromise { + return (this._executionEngine === ExecutionEngine.Process + ? this.computeLegacyConfiguration(workspaceFolder) + : this.computeConfiguration(workspaceFolder)). + then((workspaceFolderConfiguration) => { + if (!workspaceFolderConfiguration || !workspaceFolderConfiguration.config || workspaceFolderConfiguration.hasErrors) { + return TPromise.as({ workspaceFolder, set: undefined, configurations: undefined, hasErrors: workspaceFolderConfiguration ? workspaceFolderConfiguration.hasErrors : false }); + } + return ProblemMatcherRegistry.onReady().then((): WorkspaceFolderTaskResult => { + let problemReporter = new ProblemReporter(this._outputChannel); + let parseResult = TaskConfig.parse(workspaceFolder, workspaceFolderConfiguration.config, problemReporter); + let hasErrors = false; + if (!parseResult.validationStatus.isOK()) { + hasErrors = true; + this.showOutput(); + } + if (problemReporter.status.isFatal()) { + problemReporter.fatal(nls.localize('TaskSystem.configurationErrors', 'Error: the provided task configuration has validation errors and can\'t not be used. Please correct the errors first.')); + return { workspaceFolder, set: undefined, configurations: undefined, hasErrors }; + } + let customizedTasks: { byIdentifier: IStringDictionary; }; + if (parseResult.configured && parseResult.configured.length > 0) { + customizedTasks = { + byIdentifier: Object.create(null) + }; + for (let task of parseResult.configured) { + customizedTasks.byIdentifier[task.configures._key] = task; + } + } + return { workspaceFolder, set: { tasks: parseResult.custom }, configurations: customizedTasks, hasErrors }; + }); + }); + } + + private computeConfiguration(workspaceFolder: WorkspaceFolder): TPromise { + let { config, hasParseErrors } = this.getConfiguration(workspaceFolder); + return TPromise.as({ workspaceFolder, config, hasErrors: hasParseErrors }); + } + + private computeLegacyConfiguration(workspaceFolder: WorkspaceFolder): TPromise { + let { config, hasParseErrors } = this.getConfiguration(workspaceFolder); + if (hasParseErrors) { + return TPromise.as({ workspaceFolder: workspaceFolder, hasErrors: true, config: undefined }); + } + if (config) { + if (this.hasDetectorSupport(config)) { + return new ProcessRunnerDetector(this.fileService, this.contextService, this.configurationResolverService, config).detect(true).then((value): WorkspaceFolderConfigurationResult => { + let hasErrors = this.printStderr(value.stderr); + let detectedConfig = value.config; + if (!detectedConfig) { + return { workspaceFolder, config, hasErrors }; + } + let result: TaskConfig.ExternalTaskRunnerConfiguration = Objects.clone(config); + let configuredTasks: IStringDictionary = Object.create(null); + if (!result.tasks) { + if (detectedConfig.tasks) { + result.tasks = detectedConfig.tasks; + } + } else { + result.tasks.forEach(task => configuredTasks[task.taskName] = task); + detectedConfig.tasks.forEach((task) => { + if (!configuredTasks[task.taskName]) { + result.tasks.push(task); + } + }); + } + return { workspaceFolder, config: result, hasErrors }; + }); } else { - if (engine === ExecutionEngine.Terminal) { - configPromise = TPromise.as({ config, hasErrors: false }); + return TPromise.as({ workspaceFolder, config, hasErrors: false }); + } + } else { + return new ProcessRunnerDetector(this.fileService, this.contextService, this.configurationResolverService).detect(true).then((value) => { + let hasErrors = this.printStderr(value.stderr); + return { workspaceFolder, config: value.config, hasErrors }; + }); + } + } + + private computeWorkspaceFolders(): [WorkspaceFolder[], ExecutionEngine, JsonSchemaVersion] { + let workspaceFolders: WorkspaceFolder[] = []; + let executionEngine = ExecutionEngine.Terminal; + let schemaVersion = JsonSchemaVersion.V2_0_0; + + if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + let workspaceFolder: WorkspaceFolder = { uri: this.contextService.getWorkspace().folders[0] }; + workspaceFolders.push(workspaceFolder); + executionEngine = this.computeExecutionEngine(workspaceFolder); + schemaVersion = this.computeJsonSchemaVersion(workspaceFolder); + } else if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + for (let folder of this.contextService.getWorkspace().folders) { + let workspaceFolder = { uri: folder }; + if (schemaVersion === this.computeJsonSchemaVersion(workspaceFolder)) { + workspaceFolders.push(workspaceFolder); } else { - configPromise = new ProcessRunnerDetector(this.fileService, this.contextService, this.configurationResolverService).detect(true).then((value) => { - let hasErrors = this.printStderr(value.stderr); - return { config: value.config, hasErrors }; - }); + this._outputChannel.append(nls.localize( + 'taskService.ignoreingFolder', + 'Ignoring task configurations for workspace folder {0}. Multi root folder support requires that all folders use task version 2.0.', + folder.fsPath)); } } } - return configPromise.then((resolved) => { - return ProblemMatcherRegistry.onReady().then((): WorkspaceTaskResult => { - if (!resolved || !resolved.config) { - return { set: undefined, configurations: undefined, hasErrors: resolved !== void 0 ? resolved.hasErrors : false }; - } - let problemReporter = new ProblemReporter(this._outputChannel); - let parseResult = TaskConfig.parse(resolved.config, problemReporter); - let hasErrors = false; - if (!parseResult.validationStatus.isOK()) { - hasErrors = true; - this.showOutput(); - } - if (problemReporter.status.isFatal()) { - problemReporter.fatal(nls.localize('TaskSystem.configurationErrors', 'Error: the provided task configuration has validation errors and can\'t not be used. Please correct the errors first.')); - return { set: undefined, configurations: undefined, hasErrors }; - } - let customizedTasks: { byIdentifier: IStringDictionary; }; - if (parseResult.configured && parseResult.configured.length > 0) { - customizedTasks = { - byIdentifier: Object.create(null) - }; - for (let task of parseResult.configured) { - customizedTasks.byIdentifier[task.configures._key] = task; - } - } - return { set: { tasks: parseResult.custom }, configurations: customizedTasks, hasErrors }; - }); - }); + return [workspaceFolders, executionEngine, schemaVersion]; } - private getExecutionEngine(): ExecutionEngine { - let { config } = this.getConfiguration(); + private computeExecutionEngine(workspaceFolder: WorkspaceFolder): ExecutionEngine { + let { config } = this.getConfiguration(workspaceFolder); if (!config) { return ExecutionEngine._default; } return TaskConfig.ExecutionEngine.from(config); } - private getJsonSchemaVersion(): JsonSchemaVersion { - let { config } = this.getConfiguration(); + private computeJsonSchemaVersion(workspaceFolder: WorkspaceFolder): JsonSchemaVersion { + let { config } = this.getConfiguration(workspaceFolder); if (!config) { return JsonSchemaVersion.V2_0_0; } return TaskConfig.JsonSchemaVersion.from(config); } - private getConfiguration(): { config: TaskConfig.ExternalTaskRunnerConfiguration; hasParseErrors: boolean } { - let result = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? this.configurationService.getConfiguration('tasks', { resource: this.contextService.getWorkspace().folders[0] }) : undefined; + private getConfiguration(workspaceFolder: WorkspaceFolder): { config: TaskConfig.ExternalTaskRunnerConfiguration; hasParseErrors: boolean } { + let result = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY + ? this.configurationService.getConfiguration('tasks', { resource: workspaceFolder.uri }) + : undefined; if (!result) { return { config: undefined, hasParseErrors: false }; } @@ -1585,7 +1696,7 @@ class TaskService extends EventEmitter implements ITaskService { if (this._taskSystem) { return this._taskSystem instanceof TerminalTaskSystem; } - return this.getExecutionEngine() === ExecutionEngine.Terminal; + return this._executionEngine === ExecutionEngine.Terminal; } private hasDetectorSupport(config: TaskConfig.ExternalTaskRunnerConfiguration): boolean { @@ -1797,7 +1908,7 @@ class TaskService extends EventEmitter implements ITaskService { if (!this.canRunCommand()) { return; } - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V0_1_0) { + if (this._schemaVersion === JsonSchemaVersion.V0_1_0) { this.build(); return; } @@ -1840,7 +1951,7 @@ class TaskService extends EventEmitter implements ITaskService { if (!this.canRunCommand()) { return; } - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V0_1_0) { + if (this._schemaVersion === JsonSchemaVersion.V0_1_0) { this.runTest(); return; } @@ -1946,7 +2057,7 @@ class TaskService extends EventEmitter implements ITaskService { if (!this.canRunCommand()) { return; } - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V2_0_0) { + if (this._schemaVersion === JsonSchemaVersion.V2_0_0) { this.tasks().then((tasks => { if (tasks.length === 0) { this.configureBuildTask().run(); @@ -1981,7 +2092,7 @@ class TaskService extends EventEmitter implements ITaskService { if (!this.canRunCommand()) { return; } - if (this.getJsonSchemaVersion() === JsonSchemaVersion.V2_0_0) { + if (this._schemaVersion === JsonSchemaVersion.V2_0_0) { this.tasks().then((tasks => { if (tasks.length === 0) { this.configureAction().run(); diff --git a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts index 513e890116a..9bd43c3080f 100644 --- a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts @@ -561,6 +561,7 @@ function _freeze(this: void, target: T, properties: MetaData[]): Read } interface ParseContext { + workspaceFolder: Tasks.WorkspaceFolder; problemReporter: IProblemReporter; namedProblemMatchers: IStringDictionary; uuidMap: UUIDMap; @@ -1160,6 +1161,7 @@ namespace ConfiguringTask { } let taskIdentifier = TaskIdentifier.from(identifier); let configElement: Tasks.TaskSourceConfigElement = { + workspaceFolder: context.workspaceFolder, file: '.vscode\\tasks.json', index, element: external @@ -1220,7 +1222,7 @@ namespace CustomTask { let result: Tasks.CustomTask = { type: 'custom', _id: context.uuidMap.getUUID(taskName), - _source: Objects.assign({}, source, { config: { index, element: external, file: '.vscode\\tasks.json' } }), + _source: Objects.assign({}, source, { config: { index, element: external, file: '.vscode\\tasks.json', workspaceFolder: context.workspaceFolder } }), _label: taskName, name: taskName, identifier: taskName, @@ -1678,10 +1680,12 @@ class UUIDMap { class ConfigurationParser { + private workspaceFolder: Tasks.WorkspaceFolder; private problemReporter: IProblemReporter; private uuidMap: UUIDMap; - constructor(problemReporter: IProblemReporter, uuidMap: UUIDMap) { + constructor(workspaceFolder: Tasks.WorkspaceFolder, problemReporter: IProblemReporter, uuidMap: UUIDMap) { + this.workspaceFolder = workspaceFolder; this.problemReporter = problemReporter; this.uuidMap = uuidMap; } @@ -1693,6 +1697,7 @@ class ConfigurationParser { this.problemReporter.clearOutput(); } let context: ParseContext = { + workspaceFolder: this.workspaceFolder, problemReporter: this.problemReporter, uuidMap: this.uuidMap, namedProblemMatchers: undefined, @@ -1751,7 +1756,7 @@ class ConfigurationParser { let isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined; let task: Tasks.CustomTask = { _id: context.uuidMap.getUUID(globals.command.name), - _source: Objects.assign({}, source, { config: { index: -1, element: fileConfig } }), + _source: Objects.assign({}, source, { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }), _label: globals.command.name, type: 'custom', name: globals.command.name, @@ -1783,11 +1788,16 @@ class ConfigurationParser { } } -let uuidMap: UUIDMap = new UUIDMap(); -export function parse(configuration: ExternalTaskRunnerConfiguration, logger: IProblemReporter): ParseResult { +let uuidMaps: Map = new Map(); +export function parse(workspaceFolder: Tasks.WorkspaceFolder, configuration: ExternalTaskRunnerConfiguration, logger: IProblemReporter): ParseResult { + let uuidMap = uuidMaps.get(workspaceFolder.uri.toString()); + if (!uuidMap) { + uuidMap = new UUIDMap(); + uuidMaps.set(workspaceFolder.uri.toString(), uuidMap); + } try { uuidMap.start(); - return (new ConfigurationParser(logger, uuidMap)).run(configuration); + return (new ConfigurationParser(workspaceFolder, logger, uuidMap)).run(configuration); } finally { uuidMap.finish(); } @@ -1801,35 +1811,6 @@ export function getTaskIdentifier(value: TaskIdentifier): Tasks.TaskIdentifier { return TaskIdentifier.from(value); } -export function findTaskIndex(fileConfig: ExternalTaskRunnerConfiguration, task: Tasks.Task): number { - if (!fileConfig || !fileConfig.tasks) { - return undefined; - } - if (fileConfig.tasks.length === 0) { - return -1; - } - let localMap = new UUIDMap(uuidMap); - let context: ParseContext = { - problemReporter: this.problemReporter, - uuidMap: localMap, - namedProblemMatchers: undefined, - engine: ExecutionEngine.from(fileConfig), - schemaVersion: JsonSchemaVersion.from(fileConfig) - }; - try { - localMap.start(); - let tasks = TaskParser.quickParse(fileConfig.tasks, context); - for (let i = 0; i < tasks.length; i++) { - if (task._id === tasks[i]._id) { - return i; - } - } - return -1; - } finally { - localMap.finish(); - } -} - /* class VersionConverter { constructor(private problemReporter: IProblemReporter) { diff --git a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts index c89319b81dd..c9601239fb1 100644 --- a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import URI from 'vs/base/common/uri'; import * as assert from 'assert'; import Severity from 'vs/base/common/severity'; import * as UUID from 'vs/base/common/uuid'; @@ -177,7 +178,7 @@ class CustomTaskBuilder { this.commandBuilder = new CommandConfigurationBuilder(this, command); this.result = { _id: name, - _source: { kind: Tasks.TaskSourceKind.Workspace, label: 'workspace', config: { element: undefined, index: -1, file: '.vscode/tasks.json' } }, + _source: { kind: Tasks.TaskSourceKind.Workspace, label: 'workspace', config: { workspaceFolder: { uri: undefined }, element: undefined, index: -1, file: '.vscode/tasks.json' } }, _label: name, type: 'custom', identifier: name, @@ -347,7 +348,7 @@ class PatternBuilder { function testDefaultProblemMatcher(external: ExternalTaskRunnerConfiguration, resolved: number) { let reporter = new ProblemReporter(); - let result = parse(external, reporter); + let result = parse({ uri: URI.file('/Workspace/folderOne') }, external, reporter); assert.ok(!reporter.receivedMessage); assert.strictEqual(result.custom.length, 1); let task = result.custom[0]; @@ -358,7 +359,7 @@ function testDefaultProblemMatcher(external: ExternalTaskRunnerConfiguration, re function testConfiguration(external: ExternalTaskRunnerConfiguration, builder: ConfiguationBuilder): void { builder.done(); let reporter = new ProblemReporter(); - let result = parse(external, reporter); + let result = parse({ uri: URI.file('/Workspace/folderOne') }, external, reporter); if (reporter.receivedMessage) { assert.ok(false, reporter.lastMessage); } diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts index ba9f623fb9f..77d9fed7145 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts @@ -152,7 +152,7 @@ suite('Workbench - TerminalLinkHandler', () => { linkUrls.forEach(linkUrl => { supportedLinkFormats.forEach(linkFormatInfo => { - console.log('linkFormatInfo: ', linkFormatInfo); + // console.log('linkFormatInfo: ', linkFormatInfo); testLink( strings.format(linkFormatInfo.urlFormat, linkUrl, linkFormatInfo.line, linkFormatInfo.column), linkUrl, diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 0d84a8a03d8..6e950396b5c 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -12,7 +12,7 @@ import encoding = require('vs/base/node/encoding'); import errors = require('vs/base/common/errors'); import uri from 'vs/base/common/uri'; import { toResource } from 'vs/workbench/common/editor'; -import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions } from 'vs/platform/files/common/files'; import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -224,8 +224,8 @@ export class FileService implements IFileService { return this.raw.copyFile(source, target, overwrite); } - public createFile(resource: uri, content?: string): TPromise { - return this.raw.createFile(resource, content); + public createFile(resource: uri, content?: string, options?: ICreateFileOptions): TPromise { + return this.raw.createFile(resource, content, options); } public createFolder(resource: uri): TPromise { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 02be0c0be68..5d08ba12177 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -11,7 +11,7 @@ import os = require('os'); import crypto = require('crypto'); import assert = require('assert'); -import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, MAX_FILE_SIZE, FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; +import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, MAX_FILE_SIZE, FileChangesEvent, IFilesConfiguration, ICreateFileOptions } from 'vs/platform/files/common/files'; import { isEqualOrParent } from 'vs/base/common/paths'; import { ResourceMap } from 'vs/base/common/map'; import arrays = require('vs/base/common/arrays'); @@ -220,7 +220,7 @@ export class FileService implements IFileService { // Guard early against attempts to resolve an invalid file path if (resource.scheme !== 'file' || !resource.fsPath) { return TPromise.wrapError(new FileOperationError( - nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString()), + nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)), FileOperationResult.FILE_INVALID_PATH )); } @@ -292,7 +292,7 @@ export class FileService implements IFileService { // Return if file not found if (!exists) { return TPromise.wrapError(new FileOperationError( - nls.localize('fileNotFoundError', "File not found ({0})", resource.toString()), + nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), FileOperationResult.FILE_NOT_FOUND )); } @@ -381,15 +381,33 @@ export class FileService implements IFileService { }); } - public createFile(resource: uri, content: string = ''): TPromise { + public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); - // Create file - return this.updateContent(resource, content).then(result => { + let checkFilePromise: TPromise; + if (options.overwrite) { + checkFilePromise = TPromise.as(false); + } else { + checkFilePromise = pfs.exists(absolutePath); + } - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); + // Check file exists + return checkFilePromise.then(exists => { + if (exists && !options.overwrite) { + return TPromise.wrapError(new FileOperationError( + nls.localize('fileExists', "File to create already exits ({0})", resource.toString(true)), + FileOperationResult.FILE_MODIFIED_SINCE + )); + } - return result; + // Create file + return this.updateContent(resource, content).then(result => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); + + return result; + }); }); } diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index e345b6a0d2d..c9ebdeabde9 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -71,6 +71,45 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('createFile (does not overwrite by default)', function (done: () => void) { + const contents = 'Hello World'; + const resource = uri.file(path.join(testDir, 'test.txt')); + + fs.writeFileSync(resource.fsPath, ''); // create file + + service.createFile(resource, contents).done(null, error => { + assert.ok(error); + + done(); + }); + }); + + test('createFile (allows to overwrite existing)', function (done: () => void) { + let event: FileOperationEvent; + const toDispose = service.onAfterOperation(e => { + event = e; + }); + + const contents = 'Hello World'; + const resource = uri.file(path.join(testDir, 'test.txt')); + + fs.writeFileSync(resource.fsPath, ''); // create file + + service.createFile(resource, contents, { overwrite: true }).done(s => { + assert.equal(s.name, 'test.txt'); + assert.equal(fs.existsSync(s.resource.fsPath), true); + assert.equal(fs.readFileSync(s.resource.fsPath), contents); + + assert.ok(event); + assert.equal(event.resource.fsPath, resource.fsPath); + assert.equal(event.operation, FileOperation.CREATE); + assert.equal(event.target.resource.fsPath, resource.fsPath); + toDispose.dispose(); + + done(); + }, error => onError(error, done)); + }); + test('createFolder', function (done: () => void) { let event: FileOperationEvent; const toDispose = service.onAfterOperation(e => { diff --git a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts index 8233a577d9e..263f48a9b35 100644 --- a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts @@ -167,6 +167,7 @@ const schema: IJSONSchema = { }, fontColor: { type: 'string', + format: 'color', description: nls.localize('schema.fontColor', 'When using a glyph font: The color to use.') }, fontSize: { diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts index 66589da8664..7a889e4f0df 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -14,10 +14,10 @@ import * as Platform from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import URI from 'vs/base/common/uri'; const NullThemeService = new TestThemeService(); @@ -165,10 +165,10 @@ suite('Workbench BaseEditor', () => { let inst = new TestInstantiationService(); - inst.createInstance(EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', '', PLAINTEXT_MODE_ID, false)), 'id').then(editor => { + inst.createInstance(EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake'))), 'id').then(editor => { assert.strictEqual(editor.getId(), 'myEditor'); - return inst.createInstance(EditorRegistry.getEditor(inst.createInstance(ResourceEditorInput, 'fake', '', '', PLAINTEXT_MODE_ID, false)), 'id').then(editor => { + return inst.createInstance(EditorRegistry.getEditor(inst.createInstance(ResourceEditorInput, 'fake', '', URI.file('/fake'))), 'id').then(editor => { assert.strictEqual(editor.getId(), 'myOtherEditor'); (EditorRegistry).setEditors(oldEditors); @@ -186,7 +186,7 @@ suite('Workbench BaseEditor', () => { let inst = new TestInstantiationService(); - inst.createInstance(EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', '', PLAINTEXT_MODE_ID, false)), 'id').then(editor => { + inst.createInstance(EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake'))), 'id').then(editor => { assert.strictEqual('myOtherEditor', editor.getId()); (EditorRegistry).setEditors(oldEditors); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index d9ef1d58aff..80a54b01068 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -34,7 +34,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEditorGroupService, GroupArrangement, GroupOrientation, IEditorTabOptions, IMoveOptions } from 'vs/workbench/services/group/common/groupService'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -749,7 +749,7 @@ export class TestFileService implements IFileService { return TPromise.as(null); } - createFile(resource: URI, content?: string): TPromise { + createFile(resource: URI, content?: string, options?: ICreateFileOptions): TPromise { return TPromise.as(null); } diff --git a/test/smoke/package.json b/test/smoke/package.json index 6237dc93093..99c529813c7 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -19,10 +19,12 @@ "htmlparser2": "^3.9.2", "mocha": "^3.2.0", "ncp": "^2.0.0", + "portastic": "^1.0.1", "rimraf": "^2.6.1", "spectron": "~3.6.4", "strip-json-comments": "^2.0.1", "tmp": "0.0.33", - "typescript": "^2.2.2" + "typescript": "^2.2.2", + "vscode-uri": "^1.0.1" } } diff --git a/test/smoke/src/areas/debug/debug.test.ts b/test/smoke/src/areas/debug/debug.test.ts index 163b92f1487..1d5e0c080cf 100644 --- a/test/smoke/src/areas/debug/debug.test.ts +++ b/test/smoke/src/areas/debug/debug.test.ts @@ -9,7 +9,7 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as stripJsonComments from 'strip-json-comments'; -import { SpectronApplication, VSCODE_BUILD, EXTENSIONS_DIR, WORKSPACE_PATH } from '../../spectron/application'; +import { SpectronApplication, VSCODE_BUILD, EXTENSIONS_DIR } from '../../spectron/application'; describe('Debug', () => { let app: SpectronApplication = new SpectronApplication(); @@ -47,7 +47,7 @@ describe('Debug', () => { await app.workbench.openFile('app.js'); await app.workbench.debug.configure(); await app.screenCapturer.capture('launch.json file'); - const content = fs.readFileSync(path.join(WORKSPACE_PATH, '.vscode', 'launch.json'), 'utf8'); + const content = await app.workbench.editor.getEditorVisibleText(); const json = JSON.parse(stripJsonComments(content)); assert.equal(json.configurations[0].request, 'launch'); @@ -62,47 +62,65 @@ describe('Debug', () => { it('breakpoints', async function () { await app.workbench.openFile('index.js'); await app.workbench.debug.setBreakpointOnLine(6); + await app.screenCapturer.capture('breakpoints are set'); }); it('start debugging', async function () { port = await app.workbench.debug.startDebugging(); - http.get(`http://localhost:${port}`).on('error', e => void 0); + await app.screenCapturer.capture('debugging has started'); + + await new Promise((c, e) => http.get(`http://localhost:${port}`).on('response', c).on('error', e)); + await app.screenCapturer.capture('server was pinged'); await app.workbench.debug.waitForStackFrame(sf => sf.name === 'index.js' && sf.lineNumber === 6); + await app.screenCapturer.capture('debugging is paused'); }); it('focus stack frames and variables', async function () { - assert.equal(await app.workbench.debug.getLocalVariableCount(), 4); + await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 4, 'there should be 4 local variables'); + await app.workbench.debug.focusStackFrame('layer.js'); - assert.equal(await app.workbench.debug.getLocalVariableCount(), 5); + await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 5, 'there should be 5 local variables'); + await app.workbench.debug.focusStackFrame('route.js'); - assert.equal(await app.workbench.debug.getLocalVariableCount(), 3); + await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 3, 'there should be 3 local variables'); + await app.workbench.debug.focusStackFrame('index.js'); - assert.equal(await app.workbench.debug.getLocalVariableCount(), 4); + await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 4, 'there should be 4 local variables'); }); it('stepOver, stepIn, stepOut', async function () { await app.workbench.debug.stepIn(); + await app.screenCapturer.capture('debugging has stepped in'); + const first = await app.workbench.debug.waitForStackFrame(sf => sf.name === 'response.js'); await app.workbench.debug.stepOver(); + await app.screenCapturer.capture('debugging has stepped over'); + await app.workbench.debug.waitForStackFrame(sf => sf.name === 'response.js' && sf.lineNumber === first.lineNumber + 1); await app.workbench.debug.stepOut(); + await app.screenCapturer.capture('debugging has stepped out'); + await app.workbench.debug.waitForStackFrame(sf => sf.name === 'index.js' && sf.lineNumber === 7); }); - it('continue', async function () { await app.workbench.debug.continue(); - http.get(`http://localhost:${port}`).on('error', e => void 0); + await app.screenCapturer.capture('debugging has continued'); + + await new Promise((c, e) => http.get(`http://localhost:${port}`).on('response', c).on('error', e)); + await app.screenCapturer.capture('server was pinged'); + await app.workbench.debug.waitForStackFrame(sf => sf.name === 'index.js' && sf.lineNumber === 6); + await app.screenCapturer.capture('debugging is paused'); }); it('debug console', async function () { - const result = await app.workbench.debug.console('2 + 2 \n'); - assert.equal(result, '4'); + await app.client.waitFor(() => app.workbench.debug.console('2 + 2 \n'), r => r === '4', 'debug console should return 2 + 2 = 4'); }); it('stop debugging', async function () { await app.workbench.debug.stopDebugging(); + await app.screenCapturer.capture('debugging has stopped'); }); }); diff --git a/test/smoke/src/areas/editor/editor.ts b/test/smoke/src/areas/editor/editor.ts index 36b326b2f56..e37d916cedf 100644 --- a/test/smoke/src/areas/editor/editor.ts +++ b/test/smoke/src/areas/editor/editor.ts @@ -102,6 +102,13 @@ export class Editor { await this.spectron.client.waitAndClick(selector); } + public async getFocusedEditorUri(): Promise { + return this.spectron.webclient.selectorExecute(`.editor-container .monaco-editor.focused`, (elements: HTMLElement[]) => { + elements = Array.isArray(elements) ? elements : [elements]; + return elements[0].getAttribute('data-uri'); + }); + } + private async getClassSelectors(term: string, viewline: number): Promise { const result: { text: string, className: string }[] = await this.spectron.webclient.selectorExecute(`${Editor.VIEW_LINES}>:nth-child(${viewline}) span span`, elements => (Array.isArray(elements) ? elements : [elements]) diff --git a/test/smoke/src/areas/explorer/explorer.ts b/test/smoke/src/areas/explorer/explorer.ts index d59f02244d2..904afdbc170 100644 --- a/test/smoke/src/areas/explorer/explorer.ts +++ b/test/smoke/src/areas/explorer/explorer.ts @@ -25,14 +25,8 @@ export class Explorer extends Viewlet { } public async openFile(fileName: string): Promise { - let selector = `div[class="monaco-icon-label file-icon ${fileName}-name-file-icon ${this.getExtensionSelector(fileName)} explorer-item"]`; - try { - await this.spectron.client.doubleClickAndWait(selector); - await this.spectron.client.waitForElement(`.tabs-container div[aria-label="${fileName}, tab"]`); - await this.spectron.client.waitForElement(`.monaco-editor.focused`); - } catch (e) { - return Promise.reject(`Cannot fine ${fileName} in a viewlet.`); - } + await this.spectron.client.doubleClickAndWait(`div[class="monaco-icon-label file-icon ${fileName}-name-file-icon ${this.getExtensionSelector(fileName)} explorer-item"]`); + await this.spectron.workbench.waitForEditorFocus(fileName); } public getExtensionSelector(fileName: string): string { diff --git a/test/smoke/src/areas/extensions/extensions.ts b/test/smoke/src/areas/extensions/extensions.ts index 85e31128658..af4fdbb47a0 100644 --- a/test/smoke/src/areas/extensions/extensions.ts +++ b/test/smoke/src/areas/extensions/extensions.ts @@ -33,7 +33,11 @@ export class Extensions extends Viewlet { public async installExtension(name: string): Promise { await this.searchForExtension(name); - await this.spectron.client.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.install`); + + // we might want to wait for a while longer since the Marketplace can be slow + // a minute should do + await this.spectron.client.waitFor(() => this.spectron.client.click(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.install`), void 0, 'waiting for install button', 600); + await this.spectron.client.waitForElement(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.reload`); return true; } diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index 64170e8bf11..ebed884c5ed 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -21,26 +21,26 @@ describe('Localization', () => { let text = await app.workbench.explorer.getOpenEditorsViewTitle(); await app.screenCapturer.capture('Open editors title'); - assert.equal(text.toLowerCase(), 'geöffnete editoren'); + assert(/geöffnete editoren/i.test(text)); await app.workbench.search.openSearchViewlet(); text = await app.workbench.search.getTitle(); await app.screenCapturer.capture('Search title'); - assert.equal(text.toLowerCase(), 'suchen'); + assert(/suchen/i.test(text)); await app.workbench.scm.openSCMViewlet(); text = await app.workbench.scm.getTitle(); await app.screenCapturer.capture('Scm title'); - assert.equal(text.toLowerCase(), 'quellcodeverwaltung: vscode-smoketest-express (git)'); + assert(/quellcodeverwaltung/i.test(text)); await app.workbench.debug.openDebugViewlet(); text = await app.workbench.debug.getTitle(); await app.screenCapturer.capture('Debug title'); - assert.equal(text.toLowerCase(), 'debuggen'); + assert(/debuggen/i.test(text)); await app.workbench.extensions.openExtensionsViewlet(); text = await app.workbench.extensions.getTitle(); await app.screenCapturer.capture('Extensions title'); - assert.equal(text.toLowerCase(), 'erweiterungen'); + assert(/erweiterungen/i.test(text)); }); }); \ No newline at end of file diff --git a/test/smoke/src/areas/workbench/workbench.ts b/test/smoke/src/areas/workbench/workbench.ts index bf8743e6d8f..1d04da84db5 100644 --- a/test/smoke/src/areas/workbench/workbench.ts +++ b/test/smoke/src/areas/workbench/workbench.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import URI from 'vscode-uri'; import { SpectronApplication } from '../../spectron/application'; import { Explorer } from '../explorer/explorer'; import { ActivityBar } from '../activitybar/activityBar'; @@ -68,7 +70,10 @@ export class Workbench { public async waitForEditorFocus(fileName: string, untitled: boolean = false): Promise { await this.waitForActiveTab(fileName); - await this.spectron.client.waitForElement(`.editor-container[aria-label="${fileName}. ${untitled ? 'Untitled file text editor.' : 'Text file editor.'}, Group 1."] .monaco-editor textarea:focus`); + await this.spectron.client.waitFor(async () => { + const uri = await this.editor.getFocusedEditorUri(); + return uri && path.basename(URI.parse(uri).path) === fileName; + }, void 0, `Wait for editor with ${fileName} is focussed`); } public async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise { diff --git a/test/smoke/src/helpers/screenshot.ts b/test/smoke/src/helpers/screenshot.ts index e991a4fb9fe..289e600b728 100644 --- a/test/smoke/src/helpers/screenshot.ts +++ b/test/smoke/src/helpers/screenshot.ts @@ -25,7 +25,6 @@ export class ScreenCapturer { return; } - const image = await this.application.browserWindow.capturePage(); const screenshotPath = path.join( SCREENSHOTS_DIR, sanitize(this.suiteName), @@ -33,6 +32,7 @@ export class ScreenCapturer { `${ScreenCapturer.counter++}-${sanitize(name)}.png` ); + const image = await this.application.browserWindow.capturePage(); await new Promise((c, e) => mkdirp(path.dirname(screenshotPath), err => err ? e(err) : c())); await new Promise((c, e) => fs.writeFile(screenshotPath, image, err => err ? e(err) : c())); } diff --git a/test/smoke/src/spectron/application.ts b/test/smoke/src/spectron/application.ts index 72bce7b5e2f..e4dfb9932fc 100644 --- a/test/smoke/src/spectron/application.ts +++ b/test/smoke/src/spectron/application.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Application, SpectronClient as WebClient } from 'spectron'; +import { test as testPort } from 'portastic'; import { SpectronClient } from './client'; import { ScreenCapturer } from '../helpers/screenshot'; import { Workbench } from '../areas/workbench/workbench'; @@ -26,6 +27,18 @@ export enum VSCODE_BUILD { STABLE } +async function findFreePort(): Promise { + for (let i = 0; i < 10; i++) { + const port = 10000 + Math.round(Math.random() * 5000); + + if (await testPort(port)) { + return port; + } + } + + throw new Error('Could not find free port!'); +} + /** * Wraps Spectron's Application instance with its used methods. */ @@ -76,6 +89,7 @@ export class SpectronApplication { await this.startApplication(testSuiteName, codeArgs); await this.checkWindowReady(); await this.waitForWelcome(); + await this.screenCapturer.capture('Application started'); } public async reload(): Promise { @@ -87,6 +101,7 @@ export class SpectronApplication { public async stop(): Promise { if (this.spectron && this.spectron.isRunning()) { + await this.screenCapturer.capture('Stopping application'); return await this.spectron.stop(); } } @@ -114,8 +129,15 @@ export class SpectronApplication { chromeDriverArgs.push(`--user-data-dir=${path.join(this._userDir, new Date().getTime().toString())}`); + // Spectron always uses the same port number for the chrome driver + // and it handles gracefully when two instances use the same port number + // This works, but when one of the instances quits, it takes down + // chrome driver with it, leaving the other instance in DISPAIR!!! :( + const port = await findFreePort(); + this.spectron = new Application({ path: this._electronPath, + port, args, chromeDriverArgs, startTimeout: 10000, @@ -133,7 +155,7 @@ export class SpectronApplication { // Spectron opens multiple terminals in Windows platform // Workaround to focus the right window - https://github.com/electron/spectron/issues/60 await this.client.windowByIndex(1); - await this.app.browserWindow.focus(); + // await this.app.browserWindow.focus(); await this.client.waitForHTML('[id="workbench.main.container"]'); } diff --git a/test/smoke/src/spectron/client.ts b/test/smoke/src/spectron/client.ts index ddc67fb7a1e..2d48c427cd4 100644 --- a/test/smoke/src/spectron/client.ts +++ b/test/smoke/src/spectron/client.ts @@ -145,14 +145,15 @@ export class SpectronClient { return this.spectron.client.getTitle(); } - public async waitFor(func: () => T | Promise, accept?: (result: T) => boolean | Promise, timeoutMessage?: string): Promise; - public async waitFor(func: () => T | Promise, accept: (result: T) => boolean | Promise = result => !!result, timeoutMessage?: string): Promise { + public async waitFor(func: () => T | Promise, accept?: (result: T) => boolean | Promise, timeoutMessage?: string, retryCount?: number): Promise; + public async waitFor(func: () => T | Promise, accept: (result: T) => boolean | Promise = result => !!result, timeoutMessage?: string, retryCount?: number): Promise { let trial = 1; + retryCount = typeof retryCount === 'number' ? retryCount : this.retryCount; while (true) { - if (trial > this.retryCount) { - this.application.screenCapturer.capture('timeout'); - throw new Error(`${timeoutMessage}: Timed out after ${(this.retryCount * this.retryDuration) / 1000} seconds.`); + if (trial > retryCount) { + await this.application.screenCapturer.capture('timeout'); + throw new Error(`${timeoutMessage}: Timed out after ${(retryCount * this.retryDuration) / 1000} seconds.`); } let result;