diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 2043e48c7aa..a0435cb1d3e 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,9 +1,9 @@ --- name: Question -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/vscode. +about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/visual-studio-code. --- 🚨 The issue tracker is not for questions 🚨 -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/vscode. +If you have a question, please ask it on https://stackoverflow.com/questions/tagged/visual-studio-code. diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index ac3e6988afe..ab3a77f657b 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,12 +1,12 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.26.0", + "version": "1.26.1", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { "name": "ms-vscode.node-debug2", - "version": "1.25.5", + "version": "1.26.1", "repo": "https://github.com/Microsoft/vscode-node-debug2" } ] diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 3fc14da5045..abfeb7e4a4b 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -15,7 +15,6 @@ const cp = require('child_process'); var root = path.dirname(__dirname); var sha1 = util.getVersion(root); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file var semver = require('./monaco/package.json').version; var headerVersion = semver + '(' + sha1 + ')'; @@ -230,7 +229,7 @@ gulp.task('editor-distro', ['clean-editor-distro', 'compile-editor-esm', 'minify }); gulp.task('analyze-editor-distro', function () { - // @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file + // @ts-ignore var bundleInfo = require('../out-editor/bundleInfo.json'); var graph = bundleInfo.graph; var bundles = bundleInfo.bundles; diff --git a/build/gulpfile.mixin.js b/build/gulpfile.mixin.js index a370981ab3c..29cce77c5dd 100644 --- a/build/gulpfile.mixin.js +++ b/build/gulpfile.mixin.js @@ -15,7 +15,6 @@ const remote = require('gulp-remote-src'); const zip = require('gulp-vinyl-zip'); const assign = require('object-assign'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const pkg = require('../package.json'); gulp.task('mixin', function () { @@ -56,7 +55,6 @@ gulp.task('mixin', function () { .pipe(util.rebase(2)) .pipe(productJsonFilter) .pipe(buffer()) - // @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file .pipe(json(o => assign({}, require('../product.json'), o))) .pipe(productJsonFilter.restore); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 526e5e56f5a..4831c70e07b 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -25,9 +25,7 @@ const buildfile = require('../src/buildfile'); const common = require('./lib/optimize'); const root = path.dirname(__dirname); const commit = util.getVersion(root); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const packageJson = require('../package.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const product = require('../product.json'); const crypto = require('crypto'); const i18n = require('./lib/i18n'); @@ -40,12 +38,12 @@ const productionDependencies = deps.getProductionDependencies(path.dirname(__dir // @ts-ignore const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n)); const nodeModules = ['electron', 'original-fs'] + // @ts-ignore JSON checking: dependencies property is optional .concat(Object.keys(product.dependencies || {})) .concat(_.uniq(productionDependencies.map(d => d.name))) .concat(baseModules); // Build -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const builtInExtensions = require('./builtInExtensions.json'); const excludedExtensions = [ @@ -120,6 +118,8 @@ gulp.task('clean-minified-vscode', util.rimraf('out-vscode-min')); gulp.task('minify-vscode', ['clean-minified-vscode', 'optimize-index-js'], common.minifyTask('out-vscode', baseUrl)); // Package + +// @ts-ignore JSON checking: darwinCredits is optional const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSync(path.join(root, product.darwinCredits), 'utf8')); const config = { @@ -148,6 +148,8 @@ const config = { linuxExecutableName: product.applicationName, winIcon: 'resources/win32/code.ico', token: process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN'] || void 0, + + // @ts-ignore JSON checking: electronRepository is optional repo: product.electronRepository || void 0 }; @@ -255,6 +257,7 @@ function packageTask(platform, arch, opts) { .pipe(filter(['**', '!**/*.js.map'])); let version = packageJson.version; + // @ts-ignore JSON checking: quality is optional const quality = product.quality; if (quality && quality !== 'stable') { @@ -268,10 +271,8 @@ function packageTask(platform, arch, opts) { const date = new Date().toISOString(); const productJsonUpdate = { commit, date, checksums }; - try { + if (shouldSetupSettingsSearch()) { productJsonUpdate.settingsSearchBuildId = getSettingsSearchBuildId(packageJson); - } catch (err) { - console.warn(err); } const productJsonStream = gulp.src(['product.json'], { base: '.' }) @@ -286,6 +287,7 @@ function packageTask(platform, arch, opts) { const depsSrc = [ ..._.flatten(productionDependencies.map(d => path.relative(root, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`])), + // @ts-ignore JSON checking: dependencies is optional ..._.flatten(Object.keys(product.dependencies || {}).map(d => [`node_modules/${d}/**`, `!node_modules/${d}/**/{test,tests}/**`])) ]; @@ -470,9 +472,8 @@ gulp.task('upload-vscode-sourcemaps', ['minify-vscode'], () => { const allConfigDetailsPath = path.join(os.tmpdir(), 'configuration.json'); gulp.task('upload-vscode-configuration', ['generate-vscode-configuration'], () => { - const branch = process.env.BUILD_SOURCEBRANCH; - - if (!/\/master$/.test(branch) && branch.indexOf('/release/') < 0) { + if (!shouldSetupSettingsSearch()) { + const branch = process.env.BUILD_SOURCEBRANCH; console.log(`Only runs on master and release branches, not ${branch}`); return; } @@ -495,13 +496,24 @@ gulp.task('upload-vscode-configuration', ['generate-vscode-configuration'], () = })); }); -function getSettingsSearchBuildId(packageJson) { - const previous = util.getPreviousVersion(packageJson.version); +function shouldSetupSettingsSearch() { + const branch = process.env.BUILD_SOURCEBRANCH; + return branch && (/\/master$/.test(branch) || branch.indexOf('/release/') >= 0); +} +function getSettingsSearchBuildId(packageJson) { try { - const out = cp.execSync(`git rev-list ${previous}..HEAD --count`); + const branch = process.env.BUILD_SOURCEBRANCH; + const branchId = branch.indexOf('/release/') >= 0 ? 0 : + /\/master$/.test(branch) ? 1 : + 2; // Some unexpected branch + + const out = cp.execSync(`git rev-list HEAD --count`); const count = parseInt(out.toString()); - return util.versionStringToNumber(packageJson.version) * 1e4 + count; + + // + // 1.25.1, 1,234,567 commits, master = 1250112345671 + return util.versionStringToNumber(packageJson.version) * 1e8 + count * 10 + branchId; } catch (e) { throw new Error('Could not determine build number: ' + e.toString()); } diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index ecbc45df320..49f2cad8ad7 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -12,11 +12,8 @@ const shell = require('gulp-shell'); const es = require('event-stream'); const vfs = require('vinyl-fs'); const util = require('./lib/util'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const packageJson = require('../package.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const product = require('../product.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const rpmDependencies = require('../resources/linux/rpm/dependencies.json'); const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); @@ -75,7 +72,9 @@ function prepareDebPackage(arch) { const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@ARCHITECTURE@@', debArch)) + // @ts-ignore JSON checking: quality is optional .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) + // @ts-ignore JSON checking: updateUrl is optional .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(rename('DEBIAN/postinst')); @@ -133,7 +132,9 @@ function prepareRpmPackage(arch) { .pipe(replace('@@RELEASE@@', linuxPackageRevision)) .pipe(replace('@@ARCHITECTURE@@', rpmArch)) .pipe(replace('@@LICENSE@@', product.licenseName)) + // @ts-ignore JSON checking: quality is optional .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) + // @ts-ignore JSON checking: updateUrl is optional .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(replace('@@DEPENDENCIES@@', rpmDependencies[rpmArch].join(', '))) .pipe(rename('SPECS/' + product.applicationName + '.spec')); diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index a4268e7ce34..7d1ea35a6ab 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -12,9 +12,7 @@ const assert = require('assert'); const cp = require('child_process'); const _7z = require('7zip')['7z']; const util = require('./lib/util'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const pkg = require('../package.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const product = require('../product.json'); const vfs = require('vinyl-fs'); const mkdirp = require('mkdirp'); diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index c03360cf002..88266f3b901 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -17,7 +17,6 @@ const ext = require('./extensions'); const util = require('gulp-util'); const root = path.dirname(path.dirname(__dirname)); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const builtInExtensions = require('../builtInExtensions.json'); const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 7c5f15309c3..86e8db56a7d 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -22,6 +22,7 @@ import * as gulpUtil from 'gulp-util'; import * as flatmap from 'gulp-flatmap'; import * as pump from 'pump'; import * as sm from 'source-map'; +import { Language } from './i18n'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); @@ -159,6 +160,10 @@ export interface IOptimizeTaskOpts { * (out folder name) */ out: string; + /** + * (out folder name) + */ + languages?: Language[]; } export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStream { diff --git a/build/lib/test/util.test.js b/build/lib/test/util.test.js deleted file mode 100644 index ef0616173b6..00000000000 --- a/build/lib/test/util.test.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -var assert = require("assert"); -var util = require("../util"); -function getMockTagExists(tags) { - return function (tag) { return tags.indexOf(tag) >= 0; }; -} -suite('util tests', function () { - test('getPreviousVersion - patch', function () { - assert.equal(util.getPreviousVersion('1.2.3', getMockTagExists(['1.2.2', '1.2.1', '1.2.0', '1.1.0'])), '1.2.2'); - }); - test('getPreviousVersion - patch invalid', function () { - try { - util.getPreviousVersion('1.2.2', getMockTagExists(['1.2.0', '1.1.0'])); - } - catch (e) { - // expected - return; - } - throw new Error('Expected an exception'); - }); - test('getPreviousVersion - minor', function () { - assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.2', '1.1.3'])), '1.1.3'); - assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.0.0'])), '1.1.0'); - }); - test('getPreviousVersion - minor gap', function () { - assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.3'])), '1.1.1'); - }); - test('getPreviousVersion - minor invalid', function () { - try { - util.getPreviousVersion('1.2.0', getMockTagExists(['1.0.0'])); - } - catch (e) { - // expected - return; - } - throw new Error('Expected an exception'); - }); - test('getPreviousVersion - major', function () { - assert.equal(util.getPreviousVersion('2.0.0', getMockTagExists(['1.0.0', '1.1.0', '1.2.0', '1.2.1', '1.2.2'])), '1.2.2'); - }); - test('getPreviousVersion - major invalid', function () { - try { - util.getPreviousVersion('3.0.0', getMockTagExists(['1.0.0'])); - } - catch (e) { - // expected - return; - } - throw new Error('Expected an exception'); - }); -}); diff --git a/build/lib/test/util.test.ts b/build/lib/test/util.test.ts deleted file mode 100644 index 928e730f06c..00000000000 --- a/build/lib/test/util.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert = require('assert'); -import util = require('../util'); - -function getMockTagExists(tags: string[]) { - return (tag: string) => tags.indexOf(tag) >= 0; -} - -suite('util tests', () => { - test('getPreviousVersion - patch', () => { - assert.equal( - util.getPreviousVersion('1.2.3', getMockTagExists(['1.2.2', '1.2.1', '1.2.0', '1.1.0'])), - '1.2.2' - ); - }); - - test('getPreviousVersion - patch invalid', () => { - try { - util.getPreviousVersion('1.2.2', getMockTagExists(['1.2.0', '1.1.0'])); - } catch (e) { - // expected - return; - } - - throw new Error('Expected an exception'); - }); - - test('getPreviousVersion - minor', () => { - assert.equal( - util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.2', '1.1.3'])), - '1.1.3' - ); - - assert.equal( - util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.0.0'])), - '1.1.0' - ); - }); - - test('getPreviousVersion - minor gap', () => { - assert.equal( - util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.3'])), - '1.1.1' - ); - }); - - test('getPreviousVersion - minor invalid', () => { - try { - util.getPreviousVersion('1.2.0', getMockTagExists(['1.0.0'])); - } catch (e) { - // expected - return; - } - - throw new Error('Expected an exception'); - }); - - test('getPreviousVersion - major', () => { - assert.equal( - util.getPreviousVersion('2.0.0', getMockTagExists(['1.0.0', '1.1.0', '1.2.0', '1.2.1', '1.2.2'])), - '1.2.2' - ); - }); - - test('getPreviousVersion - major invalid', () => { - try { - util.getPreviousVersion('3.0.0', getMockTagExists(['1.0.0'])); - } catch (e) { - // expected - return; - } - - throw new Error('Expected an exception'); - }); -}); diff --git a/build/lib/util.js b/build/lib/util.js index 1a2d40327cd..dd6f0b56992 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -14,7 +14,6 @@ var fs = require("fs"); var _rimraf = require("rimraf"); var git = require("./git"); var VinylFile = require("vinyl"); -var cp = require("child_process"); var NoCancellationToken = { isCancellationRequested: function () { return false; } }; function incremental(streamProvider, initial, supportsCancellation) { var input = es.through(); @@ -211,62 +210,6 @@ function filter(fn) { return result; } exports.filter = filter; -function tagExists(tagName) { - try { - cp.execSync("git rev-parse " + tagName, { stdio: 'ignore' }); - return true; - } - catch (e) { - return false; - } -} -/** - * Returns the version previous to the given version. Throws if a git tag for that version doesn't exist. - * Given 1.17.2, return 1.17.1 - * 1.18.0 => 1.17.2. (or the highest 1.17.x) - * 2.0.0 => 1.18.0 (or the highest 1.x) - */ -function getPreviousVersion(versionStr, _tagExists) { - if (_tagExists === void 0) { _tagExists = tagExists; } - function getLatestTagFromBase(semverArr, componentToTest) { - var baseVersion = semverArr.join('.'); - if (!_tagExists(baseVersion)) { - throw new Error('Failed to find git tag for base version, ' + baseVersion); - } - var goodTag; - do { - goodTag = semverArr.join('.'); - semverArr[componentToTest]++; - } while (_tagExists(semverArr.join('.'))); - return goodTag; - } - var semverArr = versionStringToNumberArray(versionStr); - if (semverArr[2] > 0) { - semverArr[2]--; - var previous = semverArr.join('.'); - if (!_tagExists(previous)) { - throw new Error('Failed to find git tag for previous version, ' + previous); - } - return previous; - } - else if (semverArr[1] > 0) { - semverArr[1]--; - return getLatestTagFromBase(semverArr, 2); - } - else { - semverArr[0]--; - // Find 1.x.0 for latest x - var latestMinorVersion = getLatestTagFromBase(semverArr, 1); - // Find 1.x.y for latest y - return getLatestTagFromBase(versionStringToNumberArray(latestMinorVersion), 2); - } -} -exports.getPreviousVersion = getPreviousVersion; -function versionStringToNumberArray(versionStr) { - return versionStr - .split('.') - .map(function (s) { return parseInt(s); }); -} function versionStringToNumber(versionStr) { var semverRegex = /(\d+)\.(\d+)\.(\d+)/; var match = versionStr.match(semverRegex); diff --git a/build/lib/util.ts b/build/lib/util.ts index 9dcbbe72484..e1393b86c5b 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -17,7 +17,6 @@ import * as git from './git'; import * as VinylFile from 'vinyl'; import { ThroughStream } from 'through'; import * as sm from 'source-map'; -import * as cp from 'child_process'; export interface ICancellationToken { isCancellationRequested(): boolean; @@ -271,66 +270,6 @@ export function filter(fn: (data: any) => boolean): FilterStream { return result; } -function tagExists(tagName: string): boolean { - try { - cp.execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } -} - -/** - * Returns the version previous to the given version. Throws if a git tag for that version doesn't exist. - * Given 1.17.2, return 1.17.1 - * 1.18.0 => 1.17.2. (or the highest 1.17.x) - * 2.0.0 => 1.18.0 (or the highest 1.x) - */ -export function getPreviousVersion(versionStr: string, _tagExists = tagExists) { - function getLatestTagFromBase(semverArr: number[], componentToTest: number): string { - const baseVersion = semverArr.join('.'); - if (!_tagExists(baseVersion)) { - throw new Error('Failed to find git tag for base version, ' + baseVersion); - } - - let goodTag; - do { - goodTag = semverArr.join('.'); - semverArr[componentToTest]++; - } while (_tagExists(semverArr.join('.'))); - - return goodTag; - } - - const semverArr = versionStringToNumberArray(versionStr); - if (semverArr[2] > 0) { - semverArr[2]--; - const previous = semverArr.join('.'); - if (!_tagExists(previous)) { - throw new Error('Failed to find git tag for previous version, ' + previous); - } - - return previous; - } else if (semverArr[1] > 0) { - semverArr[1]--; - return getLatestTagFromBase(semverArr, 2); - } else { - semverArr[0]--; - - // Find 1.x.0 for latest x - const latestMinorVersion = getLatestTagFromBase(semverArr, 1); - - // Find 1.x.y for latest y - return getLatestTagFromBase(versionStringToNumberArray(latestMinorVersion), 2); - } -} - -function versionStringToNumberArray(versionStr: string): number[] { - return versionStr - .split('.') - .map(s => parseInt(s)); -} - export function versionStringToNumber(versionStr: string) { const semverRegex = /(\d+)\.(\d+)\.(\d+)/; const match = versionStr.match(semverRegex); diff --git a/build/tsconfig.json b/build/tsconfig.json index d60805e7f77..b2cc8c8a0ab 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -6,6 +6,7 @@ "removeComments": false, "preserveConstEnums": true, "sourceMap": false, + "resolveJsonModule": true, "experimentalDecorators": true, // enable JavaScript type checking for the language service // use the tsconfig.build.json for compiling wich disable JavaScript diff --git a/build/win32/OSSREADME.json b/build/win32/OSSREADME.json index e9c1e331135..6e9bb4b251a 100755 --- a/build/win32/OSSREADME.json +++ b/build/win32/OSSREADME.json @@ -545,33 +545,6 @@ ], "isProd": true }, - { - "name": "retep998/winapi-rs", - "version": "0.4.0", - "repositoryUrl": "https://github.com/retep998/winapi-rs", - "licenseDetail": [ - "Copyright (c) 2015 The winapi-rs Developers", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "isProd": true - }, { "name": "retep998/winapi-rs", "version": "0.2.8", @@ -601,7 +574,7 @@ }, { "name": "retep998/winapi-rs", - "version": "0.2.2", + "version": "0.1.1", "repositoryUrl": "https://github.com/retep998/winapi-rs", "licenseDetail": [ "Copyright (c) 2015 The winapi-rs Developers", @@ -655,7 +628,34 @@ }, { "name": "retep998/winapi-rs", - "version": "0.1.1", + "version": "0.4.0", + "repositoryUrl": "https://github.com/retep998/winapi-rs", + "licenseDetail": [ + "Copyright (c) 2015 The winapi-rs Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "isProd": true + }, + { + "name": "retep998/winapi-rs", + "version": "0.2.2", "repositoryUrl": "https://github.com/retep998/winapi-rs", "licenseDetail": [ "Copyright (c) 2015 The winapi-rs Developers", @@ -1791,4 +1791,4 @@ ], "isProd": true } -] +] \ No newline at end of file diff --git a/build/win32/code.iss b/build/win32/code.iss index 80fcbf70820..cad2e27d65e 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -92,7 +92,7 @@ Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong #if "user" == InstallTarget #define SoftwareClassesRootKey "HKCU" #else -#define SoftwareClassesRootKey "HKCR" +#define SoftwareClassesRootKey "HKLM" #endif Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles @@ -946,7 +946,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValu #define Uninstall64RootKey "HKCU64" #define Uninstall32RootKey "HKCU32" #else -#define EnvironmentRootKey "HKCR" +#define EnvironmentRootKey "HKLM" #define EnvironmentKey "System\CurrentControlSet\Control\Session Manager\Environment" #define Uninstall64RootKey "HKLM64" #define Uninstall32RootKey "HKLM32" diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index 31e6e85ffe4..d2715b864f5 100755 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index e66a313c9f6..f63d8c84087 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "jsonc-parser": "^1.0.0", - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "contributes": { "languages": [ diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 29d3d43ae80..bcf4ddb12d8 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -10,6 +10,6 @@ jsonc-parser@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-1.0.0.tgz#ddcc864ae708e60a7a6dd36daea00172fa8d9272" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 9bfc56b2e3f..dc78673bf07 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -709,7 +709,7 @@ "dependencies": { "vscode-languageclient": "^4.1.4", "vscode-languageserver-protocol-foldingprovider": "^2.0.1", - "vscode-nls": "^3.2.2" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43", diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 840422a34db..ac76cfede80 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -161,9 +161,9 @@ vscode-languageserver-types@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.7.2.tgz#aad8846f8e3e27962648554de5a8417e358f34eb" -vscode-nls@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" wrappy@1: version "1.0.2" diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index 397a5a35a09..406b25c97d2 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -449,6 +449,6 @@ "image-size": "^0.5.2", "vscode-emmet-helper": "^1.2.10", "vscode-languageserver-types": "^3.5.0", - "vscode-nls": "3.2.1" + "vscode-nls": "3.2.4" } } diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index 35b7ef8973f..040417ab779 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -2131,9 +2131,9 @@ vscode-languageserver-types@^3.6.0-next.1: version "3.6.0-next.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.6.0-next.1.tgz#98e488d3f87b666b4ee1a3d89f0023e246d358f3" -vscode-nls@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" vscode@1.0.1: version "1.0.1" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index e5c502426c0..88b339023b2 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -21,7 +21,7 @@ "jsonc-parser": "^1.0.0", "markdown-it": "^8.3.1", "parse5": "^3.0.2", - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "contributes": { "jsonValidation": [ diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 350fdcb5fe3..cdc02d1e035 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -58,6 +58,6 @@ uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/git/package.json b/extensions/git/package.json index d4a9ebb0d58..e455ec699ad 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -38,6 +38,11 @@ "dark": "resources/icons/dark/git.svg" } }, + { + "command": "git.openRepository", + "title": "%command.openRepository%", + "category": "Git" + }, { "command": "git.close", "title": "%command.close%", @@ -336,6 +341,10 @@ "command": "git.init", "when": "config.git.enabled" }, + { + "command": "git.openRepository", + "when": "config.git.enabled" + }, { "command": "git.close", "when": "config.git.enabled && gitOpenRepositoryCount != 0" @@ -881,7 +890,16 @@ "scope": "application" }, "git.autoRepositoryDetection": { - "type": "boolean", + "type": [ + "boolean", + "string" + ], + "enum": [ + true, + false, + "subFolders", + "openEditors" + ], "description": "%config.autoRepositoryDetection%", "default": true }, @@ -943,11 +961,13 @@ }, "git.enableSmartCommit": { "type": "boolean", + "scope": "resource", "description": "%config.enableSmartCommit%", "default": false }, "git.enableCommitSigning": { "type": "boolean", + "scope": "resource", "description": "%config.enableCommitSigning%", "default": false }, @@ -958,6 +978,7 @@ }, "git.promptToSaveFilesBeforeCommit": { "type": "boolean", + "scope": "resource", "default": false, "description": "%config.promptToSaveFilesBeforeCommit%" }, @@ -966,6 +987,11 @@ "default": true, "description": "%config.showInlineOpenFileAction%" }, + "git.showPushSuccessNotification": { + "type": "boolean", + "description": "%config.showPushSuccessNotification%", + "default": false + }, "git.inputValidation": { "type": "string", "enum": [ @@ -987,6 +1013,18 @@ "scope": "resource", "default": 10, "description": "%config.detectSubmodulesLimit%" + }, + "git.alwaysSignOff": { + "type": "boolean", + "scope": "resource", + "default": false, + "description": "%config.alwaysSignOff%" + }, + "git.ignoredRepositories": { + "type": "array", + "default": [], + "scope": "window", + "description": "%config.ignoredRepositories%" } } }, @@ -1140,7 +1178,7 @@ "iconv-lite": "0.4.19", "jschardet": "^1.6.0", "vscode-extension-telemetry": "0.0.17", - "vscode-nls": "^3.2.1", + "vscode-nls": "^3.2.4", "which": "^1.3.0" }, "devDependencies": { @@ -1151,4 +1189,4 @@ "@types/which": "^1.0.28", "mocha": "^3.2.0" } -} +} \ No newline at end of file diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 9b1a2332d1f..7dc623f9c8e 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -3,6 +3,7 @@ "description": "Git SCM Integration", "command.clone": "Clone", "command.init": "Initialize Repository", + "command.openRepository": "Open Repository", "command.close": "Close Repository", "command.refresh": "Refresh", "command.openChange": "Open Changes", @@ -51,7 +52,7 @@ "command.stashPopLatest": "Pop Latest Stash", "config.enabled": "Whether git is enabled", "config.path": "Path to the git executable", - "config.autoRepositoryDetection": "Whether repositories should be automatically detected", + "config.autoRepositoryDetection": "Configures when repositories should be automatically detected.", "config.autorefresh": "Whether auto refreshing is enabled", "config.autofetch": "Whether auto fetching is enabled", "config.enableLongCommitWarning": "Whether long commit messages should be warned about", @@ -68,10 +69,13 @@ "config.decorations.enabled": "Controls if Git contributes colors and badges to the explorer and the open editors view.", "config.promptToSaveFilesBeforeCommit": "Controls whether Git should check for unsaved files before committing.", "config.showInlineOpenFileAction": "Controls whether to show an inline Open File action in the Git changes view.", + "config.showPushSuccessNotification": "Controls whether to show a notification when a push is successful.", "config.inputValidation": "Controls when to show commit message input validation.", "config.detectSubmodules": "Controls whether to automatically detect git submodules.", "colors.added": "Color for added resources.", "config.detectSubmodulesLimit": "Controls the limit of git submodules detected.", + "config.alwaysSignOff": "Controls the signoff flag for all commits", + "config.ignoredRepositories": "List of git repositories to ignore", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", "colors.untracked": "Color for untracked resources.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 2d0b52d2bc6..19c3cbdbe59 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -137,6 +137,22 @@ const ImageMimetypes = [ 'image/bmp' ]; +async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[] }> { + const selection = resources.filter(s => s instanceof Resource) as Resource[]; + const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge); + const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED; + const possibleUnresolved = merge.filter(isBothAddedOrModified); + const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/)); + const unresolvedBothModified = await Promise.all(promises); + const resolved = possibleUnresolved.filter((s, i) => !unresolvedBothModified[i]); + const unresolved = [ + ...merge.filter(s => !isBothAddedOrModified(s)), + ...possibleUnresolved.filter((s, i) => unresolvedBothModified[i]) + ]; + + return { merge, resolved, unresolved }; +} + export class CommandCenter { private disposables: Disposable[]; @@ -236,7 +252,7 @@ export class CommandCenter { gitRef = indexStatus ? '' : 'HEAD'; } - const { size, object } = await repository.lstree(gitRef, uri.fsPath); + const { size, object } = await repository.getObjectDetails(gitRef, uri.fsPath); const { mimetype } = await repository.detectObjectType(object); if (mimetype === 'text/plain') { @@ -485,7 +501,28 @@ export class CommandCenter { } await this.git.init(path); - await this.model.tryOpenRepository(path); + await this.model.openRepository(path); + } + + @command('git.openRepository', { repository: false }) + async openRepository(path?: string): Promise { + if (!path) { + const result = await window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: Uri.file(os.homedir()), + openLabel: localize('open repo', "Open Repository") + }); + + if (!result || result.length === 0) { + return; + } + + path = result[0].fsPath; + } + + await this.model.openRepository(path); } @command('git.close', { repository: true }) @@ -629,20 +666,12 @@ export class CommandCenter { } const selection = resourceStates.filter(s => s instanceof Resource) as Resource[]; - const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge); - const bothModified = merge.filter(s => s.type === Status.BOTH_MODIFIED); - const promises = bothModified.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/)); - const unresolvedBothModified = await Promise.all(promises); - const resolvedConflicts = bothModified.filter((s, i) => !unresolvedBothModified[i]); - const unresolvedConflicts = [ - ...merge.filter(s => s.type !== Status.BOTH_MODIFIED), - ...bothModified.filter((s, i) => unresolvedBothModified[i]) - ]; + const { resolved, unresolved } = await categorizeResourceByResolution(selection); - if (unresolvedConflicts.length > 0) { - const message = unresolvedConflicts.length > 1 - ? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", unresolvedConflicts.length) - : localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(unresolvedConflicts[0].resourceUri.fsPath)); + if (unresolved.length > 0) { + const message = unresolved.length > 1 + ? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", unresolved.length) + : localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(unresolved[0].resourceUri.fsPath)); const yes = localize('yes', "Yes"); const pick = await window.showWarningMessage(message, { modal: true }, yes); @@ -653,7 +682,7 @@ export class CommandCenter { } const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree); - const scmResources = [...workingTree, ...resolvedConflicts, ...unresolvedConflicts]; + const scmResources = [...workingTree, ...resolved, ...unresolved]; this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`); if (!scmResources.length) { @@ -667,12 +696,12 @@ export class CommandCenter { @command('git.stageAll', { repository: true }) async stageAll(repository: Repository): Promise { const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[]; - const mergeConflicts = resources.filter(s => s.resourceGroupType === ResourceGroupType.Merge); + const { merge, unresolved } = await categorizeResourceByResolution(resources); - if (mergeConflicts.length > 0) { - const message = mergeConflicts.length > 1 - ? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", mergeConflicts.length) - : localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(mergeConflicts[0].resourceUri.fsPath)); + if (unresolved.length > 0) { + const message = unresolved.length > 1 + ? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", merge.length) + : localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(merge[0].resourceUri.fsPath)); const yes = localize('yes', "Yes"); const pick = await window.showWarningMessage(message, { modal: true }, yes); @@ -775,11 +804,18 @@ export class CommandCenter { const originalUri = toGitUri(modifiedUri, '~'); const originalDocument = await workspace.openTextDocument(originalUri); + const selectionsBeforeRevert = textEditor.selections; + const visibleRangesBeforeRevert = textEditor.visibleRanges; const result = applyLineChanges(originalDocument, modifiedDocument, changes); + const edit = new WorkspaceEdit(); edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result); workspace.applyEdit(edit); + await modifiedDocument.save(); + + textEditor.selections = selectionsBeforeRevert; + textEditor.revealRange(visibleRangesBeforeRevert[0]); } @command('git.unstage') @@ -976,7 +1012,7 @@ export class CommandCenter { getCommitMessage: () => Promise, opts?: CommitOptions ): Promise { - const config = workspace.getConfiguration('git'); + const config = workspace.getConfiguration('git', Uri.file(repository.root)); const promptToSaveFilesBeforeCommit = config.get('promptToSaveFilesBeforeCommit') === true; if (promptToSaveFilesBeforeCommit) { @@ -1030,6 +1066,10 @@ export class CommandCenter { // enable signing of commits if configurated opts.signCommit = enableCommitSigning; + if (config.get('alwaysSignOff')) { + opts.signoff = true; + } + if ( // no changes (noStagedChanges && noUnstagedChanges) @@ -1132,11 +1172,19 @@ export class CommandCenter { const HEAD = repository.HEAD; if (!HEAD || !HEAD.commit) { + window.showWarningMessage(localize('no more', "Can't undo because HEAD doesn't point to any commit.")); return; } const commit = await repository.getCommit('HEAD'); - await repository.reset('HEAD~'); + + if (commit.parents.length > 0) { + await repository.reset('HEAD~'); + } else { + await repository.deleteRef('HEAD'); + await this.unstageAll(repository); + } + repository.inputBox.value = commit.message; } @@ -1273,16 +1321,7 @@ export class CommandCenter { return; } - try { - await choice.run(repository); - } catch (err) { - if (err.gitErrorCode !== GitErrorCodes.Conflict) { - throw err; - } - - const message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing."); - await window.showWarningMessage(message); - } + await choice.run(repository); } @command('git.createTag', { repository: true }) @@ -1655,10 +1694,11 @@ export class CommandCenter { return result.catch(async err => { const options: MessageOptions = { - modal: err.gitErrorCode === GitErrorCodes.DirtyWorkTree + modal: true }; let message: string; + let type: 'error' | 'warning' = 'error'; switch (err.gitErrorCode) { case GitErrorCodes.DirtyWorkTree: @@ -1667,6 +1707,11 @@ export class CommandCenter { case GitErrorCodes.PushRejected: message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes."); break; + case GitErrorCodes.Conflict: + message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing."); + type = 'warning'; + options.modal = false; + break; default: const hint = (err.stderr || err.message || String(err)) .replace(/^error: /mi, '') @@ -1687,11 +1732,11 @@ export class CommandCenter { return; } - options.modal = true; - const outputChannel = this.outputChannel as OutputChannel; const openOutputChannelChoice = localize('open git log', "Open Git Log"); - const choice = await window.showErrorMessage(message, options, openOutputChannelChoice); + const choice = type === 'error' + ? await window.showErrorMessage(message, options, openOutputChannelChoice) + : await window.showWarningMessage(message, options, openOutputChannelChoice); if (choice === openOutputChannelChoice) { outputChannel.show(); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 71a8cbc9014..a6d79456ed2 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -336,7 +336,8 @@ export const GitErrorCodes = { NoStashFound: 'NoStashFound', LocalChangesOverwritten: 'LocalChangesOverwritten', NoUpstreamBranch: 'NoUpstreamBranch', - IsInSubmodule: 'IsInSubmodule' + IsInSubmodule: 'IsInSubmodule', + WrongCase: 'WrongCase', }; function getGitErrorCode(stderr: string): string | undefined { @@ -501,6 +502,7 @@ export class Git { export interface Commit { hash: string; message: string; + parents: string[]; } export class GitStatusParser { @@ -631,6 +633,47 @@ export function parseGitmodules(raw: string): Submodule[] { return result; } +export function parseGitCommit(raw: string): Commit | null { + const match = /^([0-9a-f]{40})\n(.*)\n([^]*)$/m.exec(raw.trim()); + if (!match) { + return null; + } + + const parents = match[2] ? match[2].split(' ') : []; + return { hash: match[1], message: match[3], parents }; +} + +interface LsTreeElement { + mode: string; + type: string; + object: string; + size: string; + file: string; +} + +export function parseLsTree(raw: string): LsTreeElement[] { + return raw.split('\n') + .filter(l => !!l) + .map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!) + .filter(m => !!m) + .map(([, mode, type, object, size, file]) => ({ mode, type, object, size, file })); +} + +interface LsFilesElement { + mode: string; + object: string; + stage: string; + file: string; +} + +export function parseLsFiles(raw: string): LsFilesElement[] { + return raw.split('\n') + .filter(l => !!l) + .map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!) + .filter(m => !!m) + .map(([, mode, object, stage, file]) => ({ mode, object, stage, file })); +} + export interface DiffOptions { cached?: boolean; } @@ -699,47 +742,72 @@ export class Repository { return Promise.reject('Can\'t open file from git'); } - const { exitCode, stdout } = await exec(child); + const { exitCode, stdout, stderr } = await exec(child); if (exitCode) { - return Promise.reject(new GitError({ + const err = new GitError({ message: 'Could not show object.', exitCode - })); + }); + + if (/exists on disk, but not in/.test(stderr)) { + err.gitErrorCode = GitErrorCodes.WrongCase; + } + + return Promise.reject(err); } return stdout; } - async lstree(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }> { + async getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }> { if (!treeish) { // index - const { stdout } = await this.run(['ls-files', '--stage', '--', path]); + const elements = await this.lsfiles(path); - const match = /^(\d+)\s+([0-9a-f]{40})\s+(\d+)/.exec(stdout); - - if (!match) { + if (elements.length === 0) { throw new GitError({ message: 'Error running ls-files' }); } - const [, mode, object] = match; + const { mode, object } = elements[0]; const catFile = await this.run(['cat-file', '-s', object]); const size = parseInt(catFile.stdout); return { mode, object, size }; } - const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]); + const elements = await this.lstree(treeish, path); - const match = /^(\d+)\s+(\w+)\s+([0-9a-f]{40})\s+(\d+)/.exec(stdout); - - if (!match) { - throw new GitError({ message: 'Error running ls-tree' }); + if (elements.length === 0) { + throw new GitError({ message: 'Error running ls-files' }); } - const [, mode, , object, size] = match; + const { mode, object, size } = elements[0]; return { mode, object, size: parseInt(size) }; } + async lstree(treeish: string, path: string): Promise { + const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]); + return parseLsTree(stdout); + } + + async lsfiles(path: string): Promise { + const { stdout } = await this.run(['ls-files', '--stage', '--', path]); + return parseLsFiles(stdout); + } + + async getGitRelativePath(ref: string, relativePath: string): Promise { + const relativePathLowercase = relativePath.toLowerCase(); + const dirname = path.posix.dirname(relativePath) + '/'; + const elements: { file: string; }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname); + const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0]; + + if (!element) { + throw new GitError({ message: 'Git relative path not found.' }); + } + + return element.file; + } + async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> { const child = await this.stream(['show', object]); const buffer = await readBytes(child.stdout, 4100); @@ -822,7 +890,7 @@ export class Repository { let mode: string; try { - const details = await this.lstree('HEAD', path); + const details = await this.getObjectDetails('HEAD', path); mode = details.mode; } catch (err) { mode = '100644'; @@ -876,27 +944,41 @@ export class Repository { try { await this.run(args, { input: message || '' }); } catch (commitErr) { - if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) { - commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges; - throw commitErr; - } + await this.handleCommitError(commitErr); + } + } - try { - await this.run(['config', '--get-all', 'user.name']); - } catch (err) { - err.gitErrorCode = GitErrorCodes.NoUserNameConfigured; - throw err; - } + async rebaseContinue(): Promise { + const args = ['rebase', '--continue']; - try { - await this.run(['config', '--get-all', 'user.email']); - } catch (err) { - err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured; - throw err; - } + try { + await this.run(args); + } catch (commitErr) { + await this.handleCommitError(commitErr); + } + } + private async handleCommitError(commitErr: any): Promise { + if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) { + commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges; throw commitErr; } + + try { + await this.run(['config', '--get-all', 'user.name']); + } catch (err) { + err.gitErrorCode = GitErrorCodes.NoUserNameConfigured; + throw err; + } + + try { + await this.run(['config', '--get-all', 'user.email']); + } catch (err) { + err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured; + throw err; + } + + throw commitErr; } async branch(name: string, checkout: boolean): Promise { @@ -914,6 +996,11 @@ export class Repository { await this.run(args); } + async deleteRef(ref: string): Promise { + const args = ['update-ref', '-d', ref]; + await this.run(args); + } + async merge(ref: string): Promise { const args = ['merge', ref]; @@ -1328,14 +1415,8 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.run(['show', '-s', '--format=%H\n%B', ref]); - const match = /^([0-9a-f]{40})\n([^]*)$/m.exec(result.stdout.trim()); - - if (!match) { - return Promise.reject('bad commit format'); - } - - return { hash: match[1], message: match[2] }; + const result = await this.run(['show', '-s', '--format=%H\n%P\n%B', ref]); + return parseGitCommit(result.stdout) || Promise.reject('bad commit format'); } async updateSubmodules(paths: string[]): Promise { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 65036c93a7b..f28f71324ca 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -91,6 +91,13 @@ export class Model { * for git repositories. */ private async scanWorkspaceFolders(): Promise { + const config = workspace.getConfiguration('git'); + const autoRepositoryDetection = config.get('autoRepositoryDetection'); + + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { + return; + } + for (const folder of workspace.workspaceFolders || []) { const root = folder.uri.fsPath; @@ -99,7 +106,7 @@ export class Model { children .filter(child => child !== '.git') - .forEach(child => this.tryOpenRepository(path.join(root, child))); + .forEach(child => this.openRepository(path.join(root, child))); } catch (err) { // noop } @@ -118,7 +125,7 @@ export class Model { @debounce(500) private eventuallyScanPossibleGitRepositories(): void { for (const path of this.possibleGitRepositoryPaths) { - this.tryOpenRepository(path); + this.openRepository(path); } this.possibleGitRepositoryPaths.clear(); @@ -139,7 +146,7 @@ export class Model { .filter(r => !activeRepositories.has(r!.repository)) .filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[]; - possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath)); + possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath)); openRepositoriesToDispose.forEach(r => r.dispose()); } @@ -153,15 +160,15 @@ export class Model { .filter(({ root }) => workspace.getConfiguration('git', root).get('enabled') !== true) .map(({ repository }) => repository); - possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath)); + possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath)); openRepositoriesToDispose.forEach(r => r.dispose()); } private onDidChangeVisibleTextEditors(editors: TextEditor[]): void { const config = workspace.getConfiguration('git'); - const enabled = config.get('autoRepositoryDetection') === true; + const autoRepositoryDetection = config.get('autoRepositoryDetection'); - if (!enabled) { + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') { return; } @@ -178,12 +185,12 @@ export class Model { return; } - this.tryOpenRepository(path.dirname(uri.fsPath)); + this.openRepository(path.dirname(uri.fsPath)); }); } @sequentialize - async tryOpenRepository(path: string): Promise { + async openRepository(path: string): Promise { if (this.getRepository(path)) { return; } @@ -207,6 +214,13 @@ export class Model { return; } + const config = workspace.getConfiguration('git'); + const ignoredRepos = new Set(config.get>('ignoredRepositories')); + + if (ignoredRepos.has(rawRoot)) { + return; + } + const repository = new Repository(this.git.open(repositoryRoot), this.globalState); this.open(repository); diff --git a/extensions/git/src/protocolHandler.ts b/extensions/git/src/protocolHandler.ts index b84d6620068..422ca3df739 100644 --- a/extensions/git/src/protocolHandler.ts +++ b/extensions/git/src/protocolHandler.ts @@ -5,16 +5,16 @@ 'use strict'; -import { ProtocolHandler, Uri, window, Disposable, commands } from 'vscode'; +import { UriHandler, Uri, window, Disposable, commands } from 'vscode'; import { dispose } from './util'; import * as querystring from 'querystring'; -export class GitProtocolHandler implements ProtocolHandler { +export class GitProtocolHandler implements UriHandler { private disposables: Disposable[] = []; constructor() { - this.disposables.push(window.registerProtocolHandler(this)); + this.disposables.push(window.registerUriHandler(this)); } handleUri(uri: Uri): void { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 5c5fd6814a3..b3af0698312 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -293,13 +293,15 @@ export enum Operation { GetCommitTemplate = 'GetCommitTemplate', DeleteBranch = 'DeleteBranch', RenameBranch = 'RenameBranch', + DeleteRef = 'DeleteRef', Merge = 'Merge', Ignore = 'Ignore', Tag = 'Tag', Stash = 'Stash', CheckIgnore = 'CheckIgnore', - LSTree = 'LSTree', - SubmoduleUpdate = 'SubmoduleUpdate' + GetObjectDetails = 'GetObjectDetails', + SubmoduleUpdate = 'SubmoduleUpdate', + RebaseContinue = 'RebaseContinue', } function isReadOnly(operation: Operation): boolean { @@ -307,7 +309,7 @@ function isReadOnly(operation: Operation): boolean { case Operation.Show: case Operation.GetCommitTemplate: case Operation.CheckIgnore: - case Operation.LSTree: + case Operation.GetObjectDetails: return true; default: return false; @@ -318,7 +320,7 @@ function shouldShowProgress(operation: Operation): boolean { switch (operation) { case Operation.Fetch: case Operation.CheckIgnore: - case Operation.LSTree: + case Operation.GetObjectDetails: case Operation.Show: return false; default: @@ -479,6 +481,22 @@ export class Repository implements Disposable { return this._submodules; } + private _rebaseCommit: Commit | undefined = undefined; + + set rebaseCommit(rebaseCommit: Commit | undefined) { + if (this._rebaseCommit && !rebaseCommit) { + this.inputBox.value = ''; + } else if (rebaseCommit && (!this._rebaseCommit || this._rebaseCommit.hash !== rebaseCommit.hash)) { + this.inputBox.value = rebaseCommit.message; + } + + this._rebaseCommit = rebaseCommit; + } + + get rebaseCommit(): Commit | undefined { + return this._rebaseCommit; + } + private _operations = new OperationsImpl(); get operations(): Operations { return this._operations; } @@ -540,6 +558,16 @@ export class Repository implements Disposable { this.disposables.push(new AutoFetcher(this, globalState)); + // https://github.com/Microsoft/vscode/issues/39039 + const onSuccessfulPush = filterEvent(this.onDidRunOperation, e => e.operation === Operation.Push && !e.error); + onSuccessfulPush(() => { + const gitConfig = workspace.getConfiguration('git'); + + if (gitConfig.get('showPushSuccessNotification')) { + window.showInformationMessage(localize('push success', "Successfully pushed.")); + } + }, null, this.disposables); + const statusBar = new StatusBarCommands(this); this.disposables.push(statusBar); statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables); @@ -553,6 +581,15 @@ export class Repository implements Disposable { } validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined { + if (this.rebaseCommit) { + if (this.rebaseCommit.message !== text) { + return { + message: localize('commit in rebase', "It's not possible to change the commit message in the middle of a rebase. Please complete the rebase operation and use interactive rebase instead."), + type: SourceControlInputBoxValidationType.Warning + }; + } + } + const config = workspace.getConfiguration('git'); const setting = config.get<'always' | 'warn' | 'off'>('inputValidation'); @@ -636,13 +673,23 @@ export class Repository implements Disposable { } async commit(message: string, opts: CommitOptions = Object.create(null)): Promise { - await this.run(Operation.Commit, async () => { - if (opts.all) { - await this.repository.add([]); - } + if (this.rebaseCommit) { + await this.run(Operation.RebaseContinue, async () => { + if (opts.all) { + await this.repository.add([]); + } - await this.repository.commit(message, opts); - }); + await this.repository.rebaseContinue(); + }); + } else { + await this.run(Operation.Commit, async () => { + if (opts.all) { + await this.repository.add([]); + } + + await this.repository.commit(message, opts); + }); + } } async clean(resources: Uri[]): Promise { @@ -730,6 +777,10 @@ export class Repository implements Disposable { await this.run(Operation.Reset, () => this.repository.reset(treeish, hard)); } + async deleteRef(ref: string): Promise { + await this.run(Operation.DeleteRef, () => this.repository.deleteRef(ref)); + } + @throttle async fetch(): Promise { await this.run(Operation.Fetch, () => this.repository.fetch()); @@ -825,13 +876,22 @@ export class Repository implements Disposable { } async show(ref: string, filePath: string): Promise { - return this.run(Operation.Show, () => { + return await this.run(Operation.Show, async () => { const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); - return this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding); + try { + return await this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.WrongCase) { + const gitRelativePath = await this.repository.getGitRelativePath(ref, relativePath); + return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); + } + + throw err; + } }); } @@ -842,8 +902,8 @@ export class Repository implements Disposable { }); } - lstree(ref: string, filePath: string): Promise<{ mode: string, object: string, size: number }> { - return this.run(Operation.LSTree, () => this.repository.lstree(ref, filePath)); + getObjectDetails(ref: string, filePath: string): Promise<{ mode: string, object: string, size: number }> { + return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath)); } detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> { @@ -1025,12 +1085,13 @@ export class Repository implements Disposable { // noop } - const [refs, remotes, submodules] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes(), this.repository.getSubmodules()]); + const [refs, remotes, submodules, rebaseCommit] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes(), this.repository.getSubmodules(), this.getRebaseCommit()]); this._HEAD = HEAD; this._refs = refs; this._remotes = remotes; this._submodules = submodules; + this.rebaseCommit = rebaseCommit; const index: Resource[] = []; const workingTree: Resource[] = []; @@ -1089,6 +1150,17 @@ export class Repository implements Disposable { this._onDidChangeStatus.fire(); } + private async getRebaseCommit(): Promise { + const rebaseHeadPath = path.join(this.repository.root, '.git', 'REBASE_HEAD'); + + try { + const rebaseHead = await new Promise((c, e) => fs.readFile(rebaseHeadPath, 'utf8', (err, result) => err ? e(err) : c(result))); + return await this.getCommit(rebaseHead.trim()); + } catch (err) { + return undefined; + } + } + private onFSChange(uri: Uri): void { const config = workspace.getConfiguration('git'); const autorefresh = config.get('autorefresh'); diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index be97b3355cb..979c961f64d 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -24,7 +24,8 @@ class CheckoutStatusBar { } get command(): Command | undefined { - const title = `$(git-branch) ${this.repository.headLabel}`; + const rebasing = !!this.repository.rebaseCommit; + const title = `$(git-branch) ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`; return { command: 'git.checkout', diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 09661eebc9c..eee43347351 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -6,7 +6,7 @@ 'use strict'; import 'mocha'; -import { GitStatusParser, parseGitmodules } from '../git'; +import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git'; import * as assert from 'assert'; suite('git', () => { @@ -175,4 +175,106 @@ suite('git', () => { ]); }); }); + + suite('parseGitCommit', () => { + test('single parent commit', function () { + const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1 +8e5a374372b8393906c7e380dbb09349c5385554 +This is a commit message.`; + + assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), { + hash: '52c293a05038d865604c2284aa8698bd087915a1', + message: 'This is a commit message.', + parents: ['8e5a374372b8393906c7e380dbb09349c5385554'] + }); + }); + + test('multiple parent commits', function () { + const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 +8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 +This is a commit message.`; + + assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), { + hash: '52c293a05038d865604c2284aa8698bd087915a1', + message: 'This is a commit message.', + parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'] + }); + }); + + test('no parent commits', function () { + const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 + +This is a commit message.`; + + assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), { + hash: '52c293a05038d865604c2284aa8698bd087915a1', + message: 'This is a commit message.', + parents: [] + }); + }); + }); + + suite('parseLsTree', function () { + test('sample', function () { + const input = `040000 tree 0274a81f8ee9ca3669295dc40f510bd2021d0043 - .vscode +100644 blob 1d487c1817262e4f20efbfa1d04c18f51b0046f6 491570 Screen Shot 2018-06-01 at 14.48.05.png +100644 blob 686c16e4f019b734655a2576ce8b98749a9ffdb9 764420 Screen Shot 2018-06-07 at 20.04.59.png +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 4 boom.txt +100644 blob 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 11 boomcaboom.txt +100644 blob a68b14060589b16d7ac75f67b905c918c03c06eb 24 file.js +100644 blob f7bcfb05af46850d780f88c069edcd57481d822d 201 file.md +100644 blob ab8b86114a051f6490f1ec5e3141b9a632fb46b5 8 hello.js +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 4 what.js +100644 blob be859e3f412fa86513cd8bebe8189d1ea1a3e46d 24 what.txt +100644 blob 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 261186 what.txt2`; + + const output = parseLsTree(input); + + assert.deepEqual(output, [ + { mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', size: '-', file: '.vscode' }, + { mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', size: '491570', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, + { mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', size: '764420', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, + { mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', size: '4', file: 'boom.txt' }, + { mode: '100644', type: 'blob', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', size: '11', file: 'boomcaboom.txt' }, + { mode: '100644', type: 'blob', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', size: '24', file: 'file.js' }, + { mode: '100644', type: 'blob', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', size: '201', file: 'file.md' }, + { mode: '100644', type: 'blob', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', size: '8', file: 'hello.js' }, + { mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', size: '4', file: 'what.js' }, + { mode: '100644', type: 'blob', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', size: '24', file: 'what.txt' }, + { mode: '100644', type: 'blob', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', size: '261186', file: 'what.txt2' } + ]); + }); + }); + + suite('parseLsFiles', function () { + test('sample', function () { + const input = `100644 7a73a41bfdf76d6f793007240d80983a52f15f97 0 .vscode/settings.json +100644 1d487c1817262e4f20efbfa1d04c18f51b0046f6 0 Screen Shot 2018-06-01 at 14.48.05.png +100644 686c16e4f019b734655a2576ce8b98749a9ffdb9 0 Screen Shot 2018-06-07 at 20.04.59.png +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 boom.txt +100644 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 0 boomcaboom.txt +100644 a68b14060589b16d7ac75f67b905c918c03c06eb 0 file.js +100644 f7bcfb05af46850d780f88c069edcd57481d822d 0 file.md +100644 ab8b86114a051f6490f1ec5e3141b9a632fb46b5 0 hello.js +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 what.js +100644 be859e3f412fa86513cd8bebe8189d1ea1a3e46d 0 what.txt +100644 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 0 what.txt2`; + + const output = parseLsFiles(input); + + assert.deepEqual(output, [ + { mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' }, + { mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, + { mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, + { mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'boom.txt' }, + { mode: '100644', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', stage: '0', file: 'boomcaboom.txt' }, + { mode: '100644', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', stage: '0', file: 'file.js' }, + { mode: '100644', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', stage: '0', file: 'file.md' }, + { mode: '100644', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', stage: '0', file: 'hello.js' }, + { mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'what.js' }, + { mode: '100644', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', stage: '0', file: 'what.txt' }, + { mode: '100644', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', stage: '0', file: 'what.txt2' }, + ]); + }); + }); }); \ No newline at end of file diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 3fb8a6f087a..d8a4d64e743 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -263,9 +263,9 @@ vscode-extension-telemetry@0.0.17: dependencies: applicationinsights "1.0.1" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" which@^1.3.0: version "1.3.0" diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index cfc88d5dea3..18c5c21e0cb 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -16,7 +16,7 @@ "watch": "gulp watch-extension:grunt" }, "dependencies": { - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43" diff --git a/extensions/grunt/yarn.lock b/extensions/grunt/yarn.lock index 112e5f2ac8d..573098d7d26 100644 --- a/extensions/grunt/yarn.lock +++ b/extensions/grunt/yarn.lock @@ -6,6 +6,6 @@ version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index bc53fe127f7..65d9197a0d9 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -16,7 +16,7 @@ "watch": "gulp watch-extension:gulp" }, "dependencies": { - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43" diff --git a/extensions/gulp/yarn.lock b/extensions/gulp/yarn.lock index 112e5f2ac8d..573098d7d26 100644 --- a/extensions/gulp/yarn.lock +++ b/extensions/gulp/yarn.lock @@ -6,6 +6,6 @@ version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 5781a2d2faa..9ac18e38fbb 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -176,7 +176,7 @@ "vscode-extension-telemetry": "0.0.17", "vscode-languageclient": "^4.1.4", "vscode-languageserver-protocol-foldingprovider": "^2.0.1", - "vscode-nls": "^3.2.2" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43" diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 3ef45e9eff9..e9586c7ad34 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -13,7 +13,7 @@ "vscode-languageserver": "^4.1.3", "vscode-languageserver-protocol-foldingprovider": "^2.0.1", "vscode-languageserver-types": "^3.7.2", - "vscode-nls": "^3.2.2", + "vscode-nls": "^3.2.4", "vscode-uri": "^1.0.3" }, "devDependencies": { diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 25d446165f1..32e736c53c2 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -242,6 +242,10 @@ vscode-nls@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" + vscode-uri@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.1.tgz#11a86befeac3c4aa3ec08623651a3c81a6d0bbc8" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index 36c0c89cd28..1e7214749f2 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -62,9 +62,9 @@ vscode-languageserver-types@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.7.2.tgz#aad8846f8e3e27962648554de5a8417e358f34eb" -vscode-nls@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" zone.js@0.7.6: version "0.7.6" diff --git a/extensions/jake/package.json b/extensions/jake/package.json index 2330f353026..6befa9fbd9e 100644 --- a/extensions/jake/package.json +++ b/extensions/jake/package.json @@ -16,7 +16,7 @@ "watch": "gulp watch-extension:jake" }, "dependencies": { - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43" diff --git a/extensions/jake/yarn.lock b/extensions/jake/yarn.lock index 112e5f2ac8d..573098d7d26 100644 --- a/extensions/jake/yarn.lock +++ b/extensions/jake/yarn.lock @@ -6,6 +6,6 @@ version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 30c1ca68b1c..8fd13accda3 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/7bf8960f7042474b10b519f39339fc527907ce16", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/88217b1c7d36ed5d35adc3099ba7978aabe2531b", "name": "JavaScript (with React support)", "scopeName": "source.js", "patterns": [ @@ -4115,7 +4115,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.js", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.js" @@ -4143,7 +4143,7 @@ "patterns": [ { "name": "entity.other.attribute-name.directive.js", - "match": "path|types|no-default-lib|name" + "match": "path|types|no-default-lib|lib|name" }, { "name": "keyword.operator.assignment.js", @@ -4664,8 +4664,8 @@ ] }, "jsx-tag-in-expression": { - "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", - "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", + "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", + "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "patterns": [ { "include": "#jsx-tag" @@ -4674,7 +4674,7 @@ }, "jsx-tag": { "name": "meta.tag.js", - "begin": "(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", + "begin": "(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "end": "(/>)|(?:())", "endCaptures": { "1": { @@ -4701,7 +4701,7 @@ }, "patterns": [ { - "begin": "(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>)", + "begin": "(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?)", "beginCaptures": { "1": { "name": "punctuation.definition.tag.begin.js" diff --git a/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json b/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json index 2327bd28a91..a7ceac55af3 100644 --- a/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/7bf8960f7042474b10b519f39339fc527907ce16", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/88217b1c7d36ed5d35adc3099ba7978aabe2531b", "name": "JavaScript (with React support)", "scopeName": "source.js.jsx", "patterns": [ @@ -4115,7 +4115,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.js.jsx", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.js.jsx" @@ -4143,7 +4143,7 @@ "patterns": [ { "name": "entity.other.attribute-name.directive.js.jsx", - "match": "path|types|no-default-lib|name" + "match": "path|types|no-default-lib|lib|name" }, { "name": "keyword.operator.assignment.js.jsx", @@ -4664,8 +4664,8 @@ ] }, "jsx-tag-in-expression": { - "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", - "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", + "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", + "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "patterns": [ { "include": "#jsx-tag" @@ -4674,7 +4674,7 @@ }, "jsx-tag": { "name": "meta.tag.js.jsx", - "begin": "(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", + "begin": "(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "end": "(/>)|(?:())", "endCaptures": { "1": { @@ -4701,7 +4701,7 @@ }, "patterns": [ { - "begin": "(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>)", + "begin": "(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?)", "beginCaptures": { "1": { "name": "punctuation.definition.tag.begin.js.jsx" diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 2e245fdafd8..8ea4b2f3d21 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -103,7 +103,7 @@ "vscode-extension-telemetry": "0.0.17", "vscode-languageclient": "^4.1.4", "vscode-languageserver-protocol-foldingprovider": "^2.0.1", - "vscode-nls": "^3.2.2" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43" diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 532345f51ee..2f67cd91401 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -16,7 +16,7 @@ "vscode-json-languageservice": "^3.1.2", "vscode-languageserver": "^4.1.3", "vscode-languageserver-protocol-foldingprovider": "^2.0.1", - "vscode-nls": "^3.2.2", + "vscode-nls": "^3.2.4", "vscode-uri": "^1.0.3" }, "devDependencies": { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index babef1f7177..d9b5875b9bc 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -118,6 +118,10 @@ vscode-nls@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" + vscode-uri@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.1.tgz#11a86befeac3c4aa3ec08623651a3c81a6d0bbc8" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 36c0c89cd28..1e7214749f2 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -62,9 +62,9 @@ vscode-languageserver-types@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.7.2.tgz#aad8846f8e3e27962648554de5a8417e358f34eb" -vscode-nls@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" zone.js@0.7.6: version "0.7.6" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f7115786331..984967f637f 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -296,7 +296,7 @@ "markdown-it": "^8.4.1", "markdown-it-named-headers": "0.0.4", "vscode-extension-telemetry": "0.0.17", - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/highlight.js": "9.1.10", diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 18ac5da44ed..c44b9715579 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -5462,9 +5462,9 @@ vscode-extension-telemetry@0.0.17: dependencies: applicationinsights "1.0.1" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" vscode@^1.1.10: version "1.1.10" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index f58bb6f2a90..81b65c21ac6 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -100,7 +100,7 @@ } }, "dependencies": { - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "8.0.33" diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index 355d778c63c..5b1dccc447c 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -6,6 +6,6 @@ version "8.0.33" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/npm/package.json b/extensions/npm/package.json index f45037ec8e7..df9c903fcb4 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -20,7 +20,7 @@ "jsonc-parser": "^1.0.0", "minimatch": "^3.0.4", "request-light": "^0.2.2", - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/minimatch": "^3.0.3", diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts index 3bf86c5b1e2..d4d5279e9a4 100644 --- a/extensions/npm/src/npmView.ts +++ b/extensions/npm/src/npmView.ts @@ -164,10 +164,14 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { private extractDebugArg(scripts: any, task: Task): [string, number] | undefined { let script: string = scripts[task.name]; - let match = script.match(/--(inspect|debug)(-brk)?(=(\d*))?/); + // matches --debug, --debug=1234, --debug-brk, debug-brk=1234, --inspect, + // --inspect=1234, --inspect-brk, --inspect-brk=1234, + // --inspect=localhost:1245, --inspect=127.0.0.1:1234, --inspect=[aa:1:0:0:0]:1234, --inspect=:1234 + let match = script.match(/--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/); + if (match) { - if (match[4]) { - return [match[1], parseInt(match[4])]; + if (match[6]) { + return [match[1], parseInt(match[6])]; } if (match[1] === 'inspect') { return [match[1], 9229]; diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 741a6fe9a22..9822a92a784 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -93,6 +93,6 @@ vscode-nls@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-2.0.2.tgz#808522380844b8ad153499af5c3b03921aea02da" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/php-language-features/package.json b/extensions/php-language-features/package.json index dc2332a328f..e39073271f3 100644 --- a/extensions/php-language-features/package.json +++ b/extensions/php-language-features/package.json @@ -77,7 +77,7 @@ "watch": "gulp watch-extension:php" }, "dependencies": { - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "7.0.43" diff --git a/extensions/php-language-features/yarn.lock b/extensions/php-language-features/yarn.lock index 112e5f2ac8d..573098d7d26 100644 --- a/extensions/php-language-features/yarn.lock +++ b/extensions/php-language-features/yarn.lock @@ -6,6 +6,6 @@ version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" diff --git a/extensions/powershell/syntaxes/powershell.tmLanguage.json b/extensions/powershell/syntaxes/powershell.tmLanguage.json index 60dee137b19..ca3fd4d5a4d 100644 --- a/extensions/powershell/syntaxes/powershell.tmLanguage.json +++ b/extensions/powershell/syntaxes/powershell.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/PowerShell/EditorSyntax/commit/6f5438611c54922ea94c81532a2dcfee72190039", + "version": "https://github.com/PowerShell/EditorSyntax/commit/146e421358945dbfbd24a9dcf56d759bdb0693db", "name": "PowerShell", "scopeName": "source.powershell", "patterns": [ @@ -71,7 +71,17 @@ }, { "begin": "(?&1 & set\"", + "c": " 2>&1 & set", "t": "source.powershell meta.scriptblock.powershell interpolated.simple.source.powershell string.quoted.double.powershell", "r": { "dark_plus": "string: #CE9178", @@ -1187,6 +1209,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "\"", + "t": "source.powershell meta.scriptblock.powershell interpolated.simple.source.powershell string.quoted.double.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": ")", "t": "source.powershell meta.scriptblock.powershell punctuation.section.group.end.powershell", @@ -1266,13 +1299,13 @@ }, { "c": "$", - "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell support.constant.automatic.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -1320,7 +1353,18 @@ } }, { - "c": "'^([^=]+)=(.*)'", + "c": "'", + "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell string.quoted.single.powershell punctuation.definition.string.begin.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "^([^=]+)=(.*)", "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell string.quoted.single.powershell", "r": { "dark_plus": "string: #CE9178", @@ -1330,6 +1374,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "'", + "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell string.quoted.single.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": ")", "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell punctuation.section.group.end.powershell", @@ -1431,13 +1486,13 @@ }, { "c": "$", - "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell support.constant.automatic.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -1508,13 +1563,13 @@ }, { "c": "$", - "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell meta.scriptblock.powershell interpolated.simple.source.powershell support.constant.automatic.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -1672,7 +1727,18 @@ } }, { - "c": "'Initializing Azure PowerShell environment...'", + "c": "'", + "t": "source.powershell string.quoted.single.powershell punctuation.definition.string.begin.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "Initializing Azure PowerShell environment...", "t": "source.powershell string.quoted.single.powershell", "r": { "dark_plus": "string: #CE9178", @@ -1683,14 +1749,25 @@ } }, { - "c": ";", - "t": "source.powershell keyword.other.statement-separator.powershell", + "c": "'", + "t": "source.powershell string.quoted.single.powershell punctuation.definition.string.end.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": ";", + "t": "source.powershell punctuation.terminator.statement.powershell", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -1870,7 +1947,18 @@ } }, { - "c": "'Please launch command under administrator account. It is needed for environment setting up and unit test.'", + "c": "'", + "t": "source.powershell meta.scriptblock.powershell string.quoted.single.powershell punctuation.definition.string.begin.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "Please launch command under administrator account. It is needed for environment setting up and unit test.", "t": "source.powershell meta.scriptblock.powershell string.quoted.single.powershell", "r": { "dark_plus": "string: #CE9178", @@ -1880,6 +1968,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "'", + "t": "source.powershell meta.scriptblock.powershell string.quoted.single.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": " ", "t": "source.powershell meta.scriptblock.powershell", @@ -1915,13 +2014,13 @@ }, { "c": ";", - "t": "source.powershell meta.scriptblock.powershell keyword.other.statement-separator.powershell", + "t": "source.powershell meta.scriptblock.powershell punctuation.terminator.statement.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -1937,18 +2036,18 @@ }, { "c": "$", - "t": "source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { "c": "env:", - "t": "source.powershell support.variable.drive.powershell", + "t": "source.powershell variable.other.readwrite.powershell support.variable.drive.powershell", "r": { "dark_plus": "support.variable: #9CDCFE", "light_plus": "support.variable: #001080", @@ -2069,18 +2168,18 @@ }, { "c": "$", - "t": "source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { "c": "env:", - "t": "source.powershell support.variable.drive.powershell", + "t": "source.powershell variable.other.readwrite.powershell support.variable.drive.powershell", "r": { "dark_plus": "support.variable: #9CDCFE", "light_plus": "support.variable: #001080", @@ -2102,13 +2201,13 @@ }, { "c": ";", - "t": "source.powershell keyword.other.statement-separator.powershell", + "t": "source.powershell punctuation.terminator.statement.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -2190,7 +2289,7 @@ }, { "c": "\"", - "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell", + "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell punctuation.definition.string.begin.powershell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -2201,18 +2300,18 @@ }, { "c": "$", - "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "variable: #9CDCFE" } }, { "c": "env:", - "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell support.variable.drive.powershell", + "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell variable.other.readwrite.powershell support.variable.drive.powershell", "r": { "dark_plus": "support.variable: #9CDCFE", "light_plus": "support.variable: #001080", @@ -2233,7 +2332,7 @@ } }, { - "c": "\\Microsoft Visual Studio 12.0\"", + "c": "\\Microsoft Visual Studio 12.0", "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell", "r": { "dark_plus": "string: #CE9178", @@ -2243,6 +2342,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "\"", + "t": "source.powershell interpolated.simple.source.powershell string.quoted.double.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": ")", "t": "source.powershell punctuation.section.group.end.powershell", @@ -2289,13 +2399,13 @@ }, { "c": "$", - "t": "source.powershell meta.scriptblock.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell meta.scriptblock.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { @@ -2321,7 +2431,18 @@ } }, { - "c": "\"12.0\"", + "c": "\"", + "t": "source.powershell meta.scriptblock.powershell string.quoted.double.powershell punctuation.definition.string.begin.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "12.0", "t": "source.powershell meta.scriptblock.powershell string.quoted.double.powershell", "r": { "dark_plus": "string: #CE9178", @@ -2331,6 +2452,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "\"", + "t": "source.powershell meta.scriptblock.powershell string.quoted.double.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": "}", "t": "source.powershell meta.scriptblock.powershell punctuation.section.braces.end.powershell", @@ -2399,13 +2531,13 @@ }, { "c": "$", - "t": "source.powershell meta.scriptblock.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell meta.scriptblock.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { @@ -2431,7 +2563,18 @@ } }, { - "c": "\"11.0\"", + "c": "\"", + "t": "source.powershell meta.scriptblock.powershell string.quoted.double.powershell punctuation.definition.string.begin.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "11.0", "t": "source.powershell meta.scriptblock.powershell string.quoted.double.powershell", "r": { "dark_plus": "string: #CE9178", @@ -2441,6 +2584,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "\"", + "t": "source.powershell meta.scriptblock.powershell string.quoted.double.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": "}", "t": "source.powershell meta.scriptblock.powershell punctuation.section.braces.end.powershell", @@ -2454,13 +2608,13 @@ }, { "c": "$", - "t": "source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { @@ -2508,7 +2662,18 @@ } }, { - "c": "'\"{0}\\Microsoft Visual Studio {1}\\VC\\vcvarsall.bat\" x64'", + "c": "'", + "t": "source.powershell string.quoted.single.powershell punctuation.definition.string.begin.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "\"{0}\\Microsoft Visual Studio {1}\\VC\\vcvarsall.bat\" x64", "t": "source.powershell string.quoted.single.powershell", "r": { "dark_plus": "string: #CE9178", @@ -2518,6 +2683,17 @@ "hc_black": "string: #CE9178" } }, + { + "c": "'", + "t": "source.powershell string.quoted.single.powershell punctuation.definition.string.end.powershell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, { "c": " ", "t": "source.powershell", @@ -2553,18 +2729,18 @@ }, { "c": "$", - "t": "source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { "c": "env:", - "t": "source.powershell support.variable.drive.powershell", + "t": "source.powershell variable.other.readwrite.powershell support.variable.drive.powershell", "r": { "dark_plus": "support.variable: #9CDCFE", "light_plus": "support.variable: #001080", @@ -2608,13 +2784,13 @@ }, { "c": "$", - "t": "source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { @@ -2630,13 +2806,13 @@ }, { "c": ";", - "t": "source.powershell keyword.other.statement-separator.powershell", + "t": "source.powershell punctuation.terminator.statement.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } }, { @@ -2685,13 +2861,13 @@ }, { "c": "$", - "t": "source.powershell keyword.other.variable.definition.powershell", + "t": "source.powershell variable.other.readwrite.powershell punctuation.definition.variable.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" } }, { @@ -2707,13 +2883,13 @@ }, { "c": ";", - "t": "source.powershell keyword.other.statement-separator.powershell", + "t": "source.powershell punctuation.terminator.statement.powershell", "r": { - "dark_plus": "keyword: #569CD6", - "light_plus": "keyword: #0000FF", - "dark_vs": "keyword: #569CD6", - "light_vs": "keyword: #0000FF", - "hc_black": "keyword: #569CD6" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" } } ] \ No newline at end of file diff --git a/extensions/search-rg/package.json b/extensions/search-rg/package.json index ce6a1668677..b149088fcf2 100644 --- a/extensions/search-rg/package.json +++ b/extensions/search-rg/package.json @@ -14,7 +14,7 @@ "categories": [], "dependencies": { "vscode-extension-telemetry": "0.0.15", - "vscode-nls": "^3.2.1", + "vscode-nls": "^3.2.4", "vscode-ripgrep": "^1.0.1" }, "devDependencies": { diff --git a/extensions/search-rg/src/cachedSearchProvider.ts b/extensions/search-rg/src/cachedSearchProvider.ts new file mode 100644 index 00000000000..8dba32717e1 --- /dev/null +++ b/extensions/search-rg/src/cachedSearchProvider.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as arrays from './common/arrays'; +import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from './common/fileSearchScorer'; +import * as strings from './common/strings'; +import { joinPath } from './utils'; + +interface IProviderArgs { + query: vscode.FileSearchQuery; + options: vscode.FileSearchOptions; + progress: vscode.Progress; + token: vscode.CancellationToken; +} + +export interface IInternalFileSearchProvider { + provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable; +} + +export class CachedSearchProvider { + + private static readonly BATCH_SIZE = 512; + + private caches: { [cacheKey: string]: Cache; } = Object.create(null); + + provideFileSearchResults(provider: IInternalFileSearchProvider, query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + const onResult = (result: IInternalFileMatch) => { + progress.report(joinPath(options.folder, result.relativePath)); + }; + + const providerArgs: IProviderArgs = { + query, options, progress, token + }; + + let sortedSearch = this.trySortedSearchFromCache(providerArgs, onResult); + if (!sortedSearch) { + const engineOpts = options.maxResults ? + { + ...options, + ...{ maxResults: 1e9 } + } : + options; + providerArgs.options = engineOpts; + + sortedSearch = this.doSortedSearch(providerArgs, provider); + } + + return sortedSearch.then(rawMatches => { + rawMatches.forEach(onResult); + }); + } + + private doSortedSearch(args: IProviderArgs, provider: IInternalFileSearchProvider): Promise { + const allResultsPromise = new Promise((c, e) => { + const results: IInternalFileMatch[] = []; + const onResult = (progress: IInternalFileMatch[]) => results.push(...progress); + + // TODO@roblou set maxResult = null + this.doSearch(args, provider, onResult, CachedSearchProvider.BATCH_SIZE) + .then(() => c(results), e); + }); + + let cache: Cache; + if (args.query.cacheKey) { + cache = this.getOrCreateCache(args.query.cacheKey); // TODO include folder in cache key + cache.resultsToSearchCache[args.query.pattern] = { finished: allResultsPromise }; + allResultsPromise.then(null, err => { + delete cache.resultsToSearchCache[args.query.pattern]; + }); + } + + return allResultsPromise.then(results => { + const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); + return this.sortResults(args, results, scorerCache); + }); + } + + private getOrCreateCache(cacheKey: string): Cache { + const existing = this.caches[cacheKey]; + if (existing) { + return existing; + } + return this.caches[cacheKey] = new Cache(); + } + + private trySortedSearchFromCache(args: IProviderArgs, onResult: (result: IInternalFileMatch) => void): Promise { + const cache = args.query.cacheKey && this.caches[args.query.cacheKey]; + if (!cache) { + return undefined; + } + + const cached = this.getResultsFromCache(cache, args.query.pattern, onResult); + if (cached) { + return cached.then((results) => this.sortResults(args, results, cache.scorerCache)); + } + + return undefined; + } + + private sortResults(args: IProviderArgs, results: IInternalFileMatch[], scorerCache: ScorerCache): Promise { + // we use the same compare function that is used later when showing the results using fuzzy scoring + // this is very important because we are also limiting the number of results by config.maxResults + // and as such we want the top items to be included in this result set if the number of items + // exceeds config.maxResults. + const preparedQuery = prepareQuery(args.query.pattern); + const compare = (matchA: IInternalFileMatch, matchB: IInternalFileMatch) => compareItemsByScore(matchA, matchB, preparedQuery, true, FileMatchItemAccessor, scorerCache); + + return arrays.topAsync(results, compare, args.options.maxResults || 0, 10000); + } + + private getResultsFromCache(cache: Cache, searchValue: string, onResult: (results: IInternalFileMatch) => void): Promise { + // Find cache entries by prefix of search value + const hasPathSep = searchValue.indexOf(path.sep) >= 0; + let cached: CacheEntry; + let wasResolved: boolean; + for (let previousSearch in cache.resultsToSearchCache) { + // If we narrow down, we might be able to reuse the cached results + if (searchValue.startsWith(previousSearch)) { + if (hasPathSep && previousSearch.indexOf(path.sep) < 0) { + continue; // since a path character widens the search for potential more matches, require it in previous search too + } + + const c = cache.resultsToSearchCache[previousSearch]; + c.finished.then(() => { wasResolved = false; }); + cached = c; + wasResolved = true; + break; + } + } + + if (!cached) { + return null; + } + + return new Promise((c, e) => { + cached.finished.then(cachedEntries => { + const cacheFilterStartTime = Date.now(); + + // Pattern match on results + let results: IInternalFileMatch[] = []; + const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); + for (let i = 0; i < cachedEntries.length; i++) { + let entry = cachedEntries[i]; + + // Check if this entry is a match for the search value + if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { + continue; + } + + results.push(entry); + } + + c(results); + }, e); + }); + } + + private doSearch(args: IProviderArgs, provider: IInternalFileSearchProvider, onResult: (result: IInternalFileMatch[]) => void, batchSize: number): Promise { + return new Promise((c, e) => { + let batch: IInternalFileMatch[] = []; + const onProviderResult = (match: string) => { + if (match) { + const internalMatch: IInternalFileMatch = { + relativePath: match, + basename: path.basename(match) + }; + + batch.push(internalMatch); + if (batchSize > 0 && batch.length >= batchSize) { + onResult(batch); + batch = []; + } + } + }; + + provider.provideFileSearchResults(args.options, { report: onProviderResult }, args.token).then(() => { + if (batch.length) { + onResult(batch); + } + + c(); + }, error => { + if (batch.length) { + onResult(batch); + } + + e(error); + }); + }); + } + + public clearCache(cacheKey: string): Promise { + delete this.caches[cacheKey]; + return Promise.resolve(undefined); + } +} + +interface IInternalFileMatch { + relativePath?: string; // Not present for extraFiles or absolute path matches + basename: string; +} + +interface CacheEntry { + finished: Promise; +} + +class Cache { + public resultsToSearchCache: { [searchValue: string]: CacheEntry } = Object.create(null); + public scorerCache: ScorerCache = Object.create(null); +} + +const FileMatchItemAccessor = new class implements IItemAccessor { + + public getItemLabel(match: IInternalFileMatch): string { + return match.basename; // e.g. myFile.txt + } + + public getItemDescription(match: IInternalFileMatch): string { + return match.relativePath.substr(0, match.relativePath.length - match.basename.length - 1); // e.g. some/path/to/file + } + + public getItemPath(match: IInternalFileMatch): string { + return match.relativePath; // e.g. some/path/to/file/myFile.txt + } +}; diff --git a/extensions/search-rg/src/common/arrays.ts b/extensions/search-rg/src/common/arrays.ts new file mode 100644 index 00000000000..d06d185dfa5 --- /dev/null +++ b/extensions/search-rg/src/common/arrays.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Asynchronous variant of `top()` allowing for splitting up work in batches between which the event loop can run. + * + * Returns the top N elements from the array. + * + * Faster than sorting the entire array when the array is a lot larger than N. + * + * @param array The unsorted array. + * @param compare A sort function for the elements. + * @param n The number of elements to return. + * @param batch The number of elements to examine before yielding to the event loop. + * @return The first n elemnts from array when sorted with compare. + */ +export function topAsync(array: T[], compare: (a: T, b: T) => number, n: number, batch: number): Promise { + // TODO@roblou cancellation + + if (n === 0) { + return Promise.resolve([]); + } + let canceled = false; + return new Promise((resolve, reject) => { + (async () => { + const o = array.length; + const result = array.slice(0, n).sort(compare); + for (let i = n, m = Math.min(n + batch, o); i < o; i = m, m = Math.min(m + batch, o)) { + if (i > n) { + await new Promise(resolve => setTimeout(resolve, 0)); // nextTick() would starve I/O. + } + if (canceled) { + throw new Error('canceled'); + } + topStep(array, compare, result, i, m); + } + return result; + })() + .then(resolve, reject); + }); +} + +function topStep(array: T[], compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { + for (const n = result.length; i < m; i++) { + const element = array[i]; + if (compare(element, result[n - 1]) < 0) { + result.pop(); + const j = findFirstInSorted(result, e => compare(element, e) < 0); + result.splice(j, 0, element); + } + } +} + +/** + * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false + * are located before all elements where p(x) is true. + * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. + */ +export function findFirstInSorted(array: T[], p: (x: T) => boolean): number { + let low = 0, high = array.length; + if (high === 0) { + return 0; // no children + } + while (low < high) { + let mid = Math.floor((low + high) / 2); + if (p(array[mid])) { + high = mid; + } else { + low = mid + 1; + } + } + return low; +} diff --git a/extensions/search-rg/src/common/charCode.ts b/extensions/search-rg/src/common/charCode.ts new file mode 100644 index 00000000000..dd1bc58f80b --- /dev/null +++ b/extensions/search-rg/src/common/charCode.ts @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030C, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031B, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033D, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR_2028 = 8232, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS + U_MACRON = 0x00AF, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00B8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02D8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE + U_OGONEK = 0x02DB, // U+02DB OGONEK + U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA + + + U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279 +} diff --git a/extensions/search-rg/src/common/comparers.ts b/extensions/search-rg/src/common/comparers.ts new file mode 100644 index 00000000000..fc6022526db --- /dev/null +++ b/extensions/search-rg/src/common/comparers.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as strings from './strings'; + +let intlFileNameCollator: Intl.Collator; +let intlFileNameCollatorIsNumeric: boolean; + +setFileNameComparer(new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })); + +export function setFileNameComparer(collator: Intl.Collator): void { + intlFileNameCollator = collator; + intlFileNameCollatorIsNumeric = collator.resolvedOptions().numeric; +} + +export function compareFileNames(one: string, other: string, caseSensitive = false): number { + if (intlFileNameCollator) { + const a = one || ''; + const b = other || ''; + const result = intlFileNameCollator.compare(a, b); + + // Using the numeric option in the collator will + // make compare(`foo1`, `foo01`) === 0. We must disambiguate. + if (intlFileNameCollatorIsNumeric && result === 0 && a !== b) { + return a < b ? -1 : 1; + } + + return result; + } + + return noIntlCompareFileNames(one, other, caseSensitive); +} + +const FileNameMatch = /^(.*?)(\.([^.]*))?$/; + +export function noIntlCompareFileNames(one: string, other: string, caseSensitive = false): number { + if (!caseSensitive) { + one = one && one.toLowerCase(); + other = other && other.toLowerCase(); + } + + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + + if (oneName !== otherName) { + return oneName < otherName ? -1 : 1; + } + + if (oneExtension === otherExtension) { + return 0; + } + + return oneExtension < otherExtension ? -1 : 1; +} + +function extractNameAndExtension(str?: string): [string, string] { + const match = str ? FileNameMatch.exec(str) : [] as RegExpExecArray; + + return [(match && match[1]) || '', (match && match[3]) || '']; +} + +export function compareAnything(one: string, other: string, lookFor: string): number { + let elementAName = one.toLowerCase(); + let elementBName = other.toLowerCase(); + + // Sort prefix matches over non prefix matches + const prefixCompare = compareByPrefix(one, other, lookFor); + if (prefixCompare) { + return prefixCompare; + } + + // Sort suffix matches over non suffix matches + let elementASuffixMatch = strings.endsWith(elementAName, lookFor); + let elementBSuffixMatch = strings.endsWith(elementBName, lookFor); + if (elementASuffixMatch !== elementBSuffixMatch) { + return elementASuffixMatch ? -1 : 1; + } + + // Understand file names + let r = compareFileNames(elementAName, elementBName); + if (r !== 0) { + return r; + } + + // Compare by name + return elementAName.localeCompare(elementBName); +} + +export function compareByPrefix(one: string, other: string, lookFor: string): number { + let elementAName = one.toLowerCase(); + let elementBName = other.toLowerCase(); + + // Sort prefix matches over non prefix matches + let elementAPrefixMatch = strings.startsWith(elementAName, lookFor); + let elementBPrefixMatch = strings.startsWith(elementBName, lookFor); + if (elementAPrefixMatch !== elementBPrefixMatch) { + return elementAPrefixMatch ? -1 : 1; + } + + // Same prefix: Sort shorter matches to the top to have those on top that match more precisely + else if (elementAPrefixMatch && elementBPrefixMatch) { + if (elementAName.length < elementBName.length) { + return -1; + } + + if (elementAName.length > elementBName.length) { + return 1; + } + } + + return 0; +} diff --git a/extensions/search-rg/src/common/fileSearchScorer.ts b/extensions/search-rg/src/common/fileSearchScorer.ts new file mode 100644 index 00000000000..d73d7c77f41 --- /dev/null +++ b/extensions/search-rg/src/common/fileSearchScorer.ts @@ -0,0 +1,619 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { stripWildcards, equalsIgnoreCase } from './strings'; +import { matchesPrefix, matchesCamelCase, createMatches, IMatch, isUpper } from './filters'; +import { compareAnything } from './comparers'; +import { CharCode } from './charCode'; + +const isWindows = process.platform === 'win32'; +const isMacintosh = (process.platform === 'darwin'); +const isLinux = (process.platform === 'linux'); + +const nativeSep = isWindows ? '\\' : '/'; + +export type Score = [number /* score */, number[] /* match positions */]; +export type ScorerCache = { [key: string]: IItemScore }; + +const NO_MATCH = 0; +const NO_SCORE: Score = [NO_MATCH, []]; + +// const DEBUG = false; +// const DEBUG_MATRIX = false; + +export function score(target: string, query: string, queryLower: string, fuzzy: boolean): Score { + if (!target || !query) { + return NO_SCORE; // return early if target or query are undefined + } + + const targetLength = target.length; + const queryLength = query.length; + + if (targetLength < queryLength) { + return NO_SCORE; // impossible for query to be contained in target + } + + // if (DEBUG) { + // console.group(`Target: ${target}, Query: ${query}`); + // } + + const targetLower = target.toLowerCase(); + + // When not searching fuzzy, we require the query to be contained fully + // in the target string contiguously. + if (!fuzzy) { + const indexOfQueryInTarget = targetLower.indexOf(queryLower); + if (indexOfQueryInTarget === -1) { + // if (DEBUG) { + // console.log(`Characters not matching consecutively ${queryLower} within ${targetLower}`); + // } + + return NO_SCORE; + } + } + + const res = doScore(query, queryLower, queryLength, target, targetLower, targetLength); + + // if (DEBUG) { + // console.log(`%cFinal Score: ${res[0]}`, 'font-weight: bold'); + // console.groupEnd(); + // } + + return res; +} + +function doScore(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number): [number, number[]] { + const scores = []; + const matches = []; + + // + // Build Scorer Matrix: + // + // The matrix is composed of query q and target t. For each index we score + // q[i] with t[i] and compare that with the previous score. If the score is + // equal or larger, we keep the match. In addition to the score, we also keep + // the length of the consecutive matches to use as boost for the score. + // + // t a r g e t + // q + // u + // e + // r + // y + // + for (let queryIndex = 0; queryIndex < queryLength; queryIndex++) { + for (let targetIndex = 0; targetIndex < targetLength; targetIndex++) { + const currentIndex = queryIndex * targetLength + targetIndex; + const leftIndex = currentIndex - 1; + const diagIndex = (queryIndex - 1) * targetLength + targetIndex - 1; + + const leftScore: number = targetIndex > 0 ? scores[leftIndex] : 0; + const diagScore: number = queryIndex > 0 && targetIndex > 0 ? scores[diagIndex] : 0; + + const matchesSequenceLength: number = queryIndex > 0 && targetIndex > 0 ? matches[diagIndex] : 0; + + // If we are not matching on the first query character any more, we only produce a + // score if we had a score previously for the last query index (by looking at the diagScore). + // This makes sure that the query always matches in sequence on the target. For example + // given a target of "ede" and a query of "de", we would otherwise produce a wrong high score + // for query[1] ("e") matching on target[0] ("e") because of the "beginning of word" boost. + let score: number; + if (!diagScore && queryIndex > 0) { + score = 0; + } else { + score = computeCharScore(query, queryLower, queryIndex, target, targetLower, targetIndex, matchesSequenceLength); + } + + // We have a score and its equal or larger than the left score + // Match: sequence continues growing from previous diag value + // Score: increases by diag score value + if (score && diagScore + score >= leftScore) { + matches[currentIndex] = matchesSequenceLength + 1; + scores[currentIndex] = diagScore + score; + } + + // We either have no score or the score is lower than the left score + // Match: reset to 0 + // Score: pick up from left hand side + else { + matches[currentIndex] = NO_MATCH; + scores[currentIndex] = leftScore; + } + } + } + + // Restore Positions (starting from bottom right of matrix) + const positions = []; + let queryIndex = queryLength - 1; + let targetIndex = targetLength - 1; + while (queryIndex >= 0 && targetIndex >= 0) { + const currentIndex = queryIndex * targetLength + targetIndex; + const match = matches[currentIndex]; + if (match === NO_MATCH) { + targetIndex--; // go left + } else { + positions.push(targetIndex); + + // go up and left + queryIndex--; + targetIndex--; + } + } + + // Print matrix + // if (DEBUG_MATRIX) { + // printMatrix(query, target, matches, scores); + // } + + return [scores[queryLength * targetLength - 1], positions.reverse()]; +} + +function computeCharScore(query: string, queryLower: string, queryIndex: number, target: string, targetLower: string, targetIndex: number, matchesSequenceLength: number): number { + let score = 0; + + if (queryLower[queryIndex] !== targetLower[targetIndex]) { + return score; // no match of characters + } + + // Character match bonus + score += 1; + + // if (DEBUG) { + // console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLower[queryIndex]} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal'); + // } + + // Consecutive match bonus + if (matchesSequenceLength > 0) { + score += (matchesSequenceLength * 5); + + // if (DEBUG) { + // console.log('Consecutive match bonus: ' + (matchesSequenceLength * 5)); + // } + } + + // Same case bonus + if (query[queryIndex] === target[targetIndex]) { + score += 1; + + // if (DEBUG) { + // console.log('Same case bonus: +1'); + // } + } + + // Start of word bonus + if (targetIndex === 0) { + score += 8; + + // if (DEBUG) { + // console.log('Start of word bonus: +8'); + // } + } + + else { + + // After separator bonus + const separatorBonus = scoreSeparatorAtPos(target.charCodeAt(targetIndex - 1)); + if (separatorBonus) { + score += separatorBonus; + + // if (DEBUG) { + // console.log('After separtor bonus: +4'); + // } + } + + // Inside word upper case bonus (camel case) + else if (isUpper(target.charCodeAt(targetIndex))) { + score += 1; + + // if (DEBUG) { + // console.log('Inside word upper case bonus: +1'); + // } + } + } + + // if (DEBUG) { + // console.groupEnd(); + // } + + return score; +} + +function scoreSeparatorAtPos(charCode: number): number { + switch (charCode) { + case CharCode.Slash: + case CharCode.Backslash: + return 5; // prefer path separators... + case CharCode.Underline: + case CharCode.Dash: + case CharCode.Period: + case CharCode.Space: + case CharCode.SingleQuote: + case CharCode.DoubleQuote: + case CharCode.Colon: + return 4; // ...over other separators + default: + return 0; + } +} + +// function printMatrix(query: string, target: string, matches: number[], scores: number[]): void { +// console.log('\t' + target.split('').join('\t')); +// for (let queryIndex = 0; queryIndex < query.length; queryIndex++) { +// let line = query[queryIndex] + '\t'; +// for (let targetIndex = 0; targetIndex < target.length; targetIndex++) { +// const currentIndex = queryIndex * target.length + targetIndex; +// line = line + 'M' + matches[currentIndex] + '/' + 'S' + scores[currentIndex] + '\t'; +// } + +// console.log(line); +// } +// } + +/** + * Scoring on structural items that have a label and optional description. + */ +export interface IItemScore { + + /** + * Overall score. + */ + score: number; + + /** + * Matches within the label. + */ + labelMatch?: IMatch[]; + + /** + * Matches within the description. + */ + descriptionMatch?: IMatch[]; +} + +const NO_ITEM_SCORE: IItemScore = Object.freeze({ score: 0 }); + +export interface IItemAccessor { + + /** + * Just the label of the item to score on. + */ + getItemLabel(item: T): string; + + /** + * The optional description of the item to score on. Can be null. + */ + getItemDescription(item: T): string; + + /** + * If the item is a file, the path of the file to score on. Can be null. + */ + getItemPath(file: T): string; +} + +const PATH_IDENTITY_SCORE = 1 << 18; +const LABEL_PREFIX_SCORE = 1 << 17; +const LABEL_CAMELCASE_SCORE = 1 << 16; +const LABEL_SCORE_THRESHOLD = 1 << 15; + +export interface IPreparedQuery { + original: string; + value: string; + lowercase: string; + containsPathSeparator: boolean; +} + +/** + * Helper function to prepare a search value for scoring in quick open by removing unwanted characters. + */ +export function prepareQuery(original: string): IPreparedQuery { + let lowercase: string; + let containsPathSeparator: boolean; + let value: string; + + if (original) { + value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace + if (isWindows) { + value = value.replace(/\//g, nativeSep); // Help Windows users to search for paths when using slash + } + + lowercase = value.toLowerCase(); + containsPathSeparator = value.indexOf(nativeSep) >= 0; + } + + return { original, value, lowercase, containsPathSeparator }; +} + +export function scoreItem(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache): IItemScore { + if (!item || !query.value) { + return NO_ITEM_SCORE; // we need an item and query to score on at least + } + + const label = accessor.getItemLabel(item); + if (!label) { + return NO_ITEM_SCORE; // we need a label at least + } + + const description = accessor.getItemDescription(item); + + let cacheHash: string; + if (description) { + cacheHash = `${label}${description}${query.value}${fuzzy}`; + } else { + cacheHash = `${label}${query.value}${fuzzy}`; + } + + const cached = cache[cacheHash]; + if (cached) { + return cached; + } + + const itemScore = doScoreItem(label, description, accessor.getItemPath(item), query, fuzzy); + cache[cacheHash] = itemScore; + + return itemScore; +} + +function doScoreItem(label: string, description: string, path: string, query: IPreparedQuery, fuzzy: boolean): IItemScore { + + // 1.) treat identity matches on full path highest + if (path && isLinux ? query.original === path : equalsIgnoreCase(query.original, path)) { + return { score: PATH_IDENTITY_SCORE, labelMatch: [{ start: 0, end: label.length }], descriptionMatch: description ? [{ start: 0, end: description.length }] : void 0 }; + } + + // We only consider label matches if the query is not including file path separators + const preferLabelMatches = !path || !query.containsPathSeparator; + if (preferLabelMatches) { + + // 2.) treat prefix matches on the label second highest + const prefixLabelMatch = matchesPrefix(query.value, label); + if (prefixLabelMatch) { + return { score: LABEL_PREFIX_SCORE, labelMatch: prefixLabelMatch }; + } + + // 3.) treat camelcase matches on the label third highest + const camelcaseLabelMatch = matchesCamelCase(query.value, label); + if (camelcaseLabelMatch) { + return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch }; + } + + // 4.) prefer scores on the label if any + const [labelScore, labelPositions] = score(label, query.value, query.lowercase, fuzzy); + if (labelScore) { + return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) }; + } + } + + // 5.) finally compute description + label scores if we have a description + if (description) { + let descriptionPrefix = description; + if (!!path) { + descriptionPrefix = `${description}${nativeSep}`; // assume this is a file path + } + + const descriptionPrefixLength = descriptionPrefix.length; + const descriptionAndLabel = `${descriptionPrefix}${label}`; + + const [labelDescriptionScore, labelDescriptionPositions] = score(descriptionAndLabel, query.value, query.lowercase, fuzzy); + if (labelDescriptionScore) { + const labelDescriptionMatches = createMatches(labelDescriptionPositions); + const labelMatch: IMatch[] = []; + const descriptionMatch: IMatch[] = []; + + // We have to split the matches back onto the label and description portions + labelDescriptionMatches.forEach(h => { + + // Match overlaps label and description part, we need to split it up + if (h.start < descriptionPrefixLength && h.end > descriptionPrefixLength) { + labelMatch.push({ start: 0, end: h.end - descriptionPrefixLength }); + descriptionMatch.push({ start: h.start, end: descriptionPrefixLength }); + } + + // Match on label part + else if (h.start >= descriptionPrefixLength) { + labelMatch.push({ start: h.start - descriptionPrefixLength, end: h.end - descriptionPrefixLength }); + } + + // Match on description part + else { + descriptionMatch.push(h); + } + }); + + return { score: labelDescriptionScore, labelMatch, descriptionMatch }; + } + } + + return NO_ITEM_SCORE; +} + +export function compareItemsByScore(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache, fallbackComparer = fallbackCompare): number { + const itemScoreA = scoreItem(itemA, query, fuzzy, accessor, cache); + const itemScoreB = scoreItem(itemB, query, fuzzy, accessor, cache); + + const scoreA = itemScoreA.score; + const scoreB = itemScoreB.score; + + // 1.) prefer identity matches + if (scoreA === PATH_IDENTITY_SCORE || scoreB === PATH_IDENTITY_SCORE) { + if (scoreA !== scoreB) { + return scoreA === PATH_IDENTITY_SCORE ? -1 : 1; + } + } + + // 2.) prefer label prefix matches + if (scoreA === LABEL_PREFIX_SCORE || scoreB === LABEL_PREFIX_SCORE) { + if (scoreA !== scoreB) { + return scoreA === LABEL_PREFIX_SCORE ? -1 : 1; + } + + const labelA = accessor.getItemLabel(itemA); + const labelB = accessor.getItemLabel(itemB); + + // prefer shorter names when both match on label prefix + if (labelA.length !== labelB.length) { + return labelA.length - labelB.length; + } + } + + // 3.) prefer camelcase matches + if (scoreA === LABEL_CAMELCASE_SCORE || scoreB === LABEL_CAMELCASE_SCORE) { + if (scoreA !== scoreB) { + return scoreA === LABEL_CAMELCASE_SCORE ? -1 : 1; + } + + const labelA = accessor.getItemLabel(itemA); + const labelB = accessor.getItemLabel(itemB); + + // prefer more compact camel case matches over longer + const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); + if (comparedByMatchLength !== 0) { + return comparedByMatchLength; + } + + // prefer shorter names when both match on label camelcase + if (labelA.length !== labelB.length) { + return labelA.length - labelB.length; + } + } + + // 4.) prefer label scores + if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) { + if (scoreB < LABEL_SCORE_THRESHOLD) { + return -1; + } + + if (scoreA < LABEL_SCORE_THRESHOLD) { + return 1; + } + } + + // 5.) compare by score + if (scoreA !== scoreB) { + return scoreA > scoreB ? -1 : 1; + } + + // 6.) scores are identical, prefer more compact matches (label and description) + const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor); + const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); + if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { + return itemBMatchDistance > itemAMatchDistance ? -1 : 1; + } + + // 7.) at this point, scores are identical and match compactness as well + // for both items so we start to use the fallback compare + return fallbackComparer(itemA, itemB, query, accessor); +} + +function computeLabelAndDescriptionMatchDistance(item: T, score: IItemScore, accessor: IItemAccessor): number { + const hasLabelMatches = (score.labelMatch && score.labelMatch.length); + const hasDescriptionMatches = (score.descriptionMatch && score.descriptionMatch.length); + + let matchStart: number = -1; + let matchEnd: number = -1; + + // If we have description matches, the start is first of description match + if (hasDescriptionMatches) { + matchStart = score.descriptionMatch[0].start; + } + + // Otherwise, the start is the first label match + else if (hasLabelMatches) { + matchStart = score.labelMatch[0].start; + } + + // If we have label match, the end is the last label match + // If we had a description match, we add the length of the description + // as offset to the end to indicate this. + if (hasLabelMatches) { + matchEnd = score.labelMatch[score.labelMatch.length - 1].end; + if (hasDescriptionMatches) { + const itemDescription = accessor.getItemDescription(item); + if (itemDescription) { + matchEnd += itemDescription.length; + } + } + } + + // If we have just a description match, the end is the last description match + else if (hasDescriptionMatches) { + matchEnd = score.descriptionMatch[score.descriptionMatch.length - 1].end; + } + + return matchEnd - matchStart; +} + +function compareByMatchLength(matchesA?: IMatch[], matchesB?: IMatch[]): number { + if ((!matchesA && !matchesB) || (!matchesA.length && !matchesB.length)) { + return 0; // make sure to not cause bad comparing when matches are not provided + } + + if (!matchesB || !matchesB.length) { + return -1; + } + + if (!matchesA || !matchesA.length) { + return 1; + } + + // Compute match length of A (first to last match) + const matchStartA = matchesA[0].start; + const matchEndA = matchesA[matchesA.length - 1].end; + const matchLengthA = matchEndA - matchStartA; + + // Compute match length of B (first to last match) + const matchStartB = matchesB[0].start; + const matchEndB = matchesB[matchesB.length - 1].end; + const matchLengthB = matchEndB - matchStartB; + + // Prefer shorter match length + return matchLengthA === matchLengthB ? 0 : matchLengthB < matchLengthA ? 1 : -1; +} + +export function fallbackCompare(itemA: T, itemB: T, query: IPreparedQuery, accessor: IItemAccessor): number { + + // check for label + description length and prefer shorter + const labelA = accessor.getItemLabel(itemA); + const labelB = accessor.getItemLabel(itemB); + + const descriptionA = accessor.getItemDescription(itemA); + const descriptionB = accessor.getItemDescription(itemB); + + const labelDescriptionALength = labelA.length + (descriptionA ? descriptionA.length : 0); + const labelDescriptionBLength = labelB.length + (descriptionB ? descriptionB.length : 0); + + if (labelDescriptionALength !== labelDescriptionBLength) { + return labelDescriptionALength - labelDescriptionBLength; + } + + // check for path length and prefer shorter + const pathA = accessor.getItemPath(itemA); + const pathB = accessor.getItemPath(itemB); + + if (pathA && pathB && pathA.length !== pathB.length) { + return pathA.length - pathB.length; + } + + // 7.) finally we have equal scores and equal length, we fallback to comparer + + // compare by label + if (labelA !== labelB) { + return compareAnything(labelA, labelB, query.value); + } + + // compare by description + if (descriptionA && descriptionB && descriptionA !== descriptionB) { + return compareAnything(descriptionA, descriptionB, query.value); + } + + // compare by path + if (pathA && pathB && pathA !== pathB) { + return compareAnything(pathA, pathB, query.value); + } + + // equal + return 0; +} \ No newline at end of file diff --git a/extensions/search-rg/src/common/filters.ts b/extensions/search-rg/src/common/filters.ts new file mode 100644 index 00000000000..63d4dcaeac3 --- /dev/null +++ b/extensions/search-rg/src/common/filters.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as strings from './strings'; +import { CharCode } from './charCode'; + +export interface IFilter { + // Returns null if word doesn't match. + (word: string, wordToMatchAgainst: string): IMatch[]; +} + +export interface IMatch { + start: number; + end: number; +} + +// Prefix + +export const matchesPrefix: IFilter = _matchesPrefix.bind(undefined, true); + +function _matchesPrefix(ignoreCase: boolean, word: string, wordToMatchAgainst: string): IMatch[] { + if (!wordToMatchAgainst || wordToMatchAgainst.length < word.length) { + return null; + } + + let matches: boolean; + if (ignoreCase) { + matches = strings.startsWithIgnoreCase(wordToMatchAgainst, word); + } else { + matches = wordToMatchAgainst.indexOf(word) === 0; + } + + if (!matches) { + return null; + } + + return word.length > 0 ? [{ start: 0, end: word.length }] : []; +} + +// CamelCase + +function isLower(code: number): boolean { + return CharCode.a <= code && code <= CharCode.z; +} + +export function isUpper(code: number): boolean { + return CharCode.A <= code && code <= CharCode.Z; +} + +function isNumber(code: number): boolean { + return CharCode.Digit0 <= code && code <= CharCode.Digit9; +} + +function isWhitespace(code: number): boolean { + return ( + code === CharCode.Space + || code === CharCode.Tab + || code === CharCode.LineFeed + || code === CharCode.CarriageReturn + ); +} + +function isAlphanumeric(code: number): boolean { + return isLower(code) || isUpper(code) || isNumber(code); +} + +function join(head: IMatch, tail: IMatch[]): IMatch[] { + if (tail.length === 0) { + tail = [head]; + } else if (head.end === tail[0].start) { + tail[0].start = head.start; + } else { + tail.unshift(head); + } + return tail; +} + +function nextAnchor(camelCaseWord: string, start: number): number { + for (let i = start; i < camelCaseWord.length; i++) { + let c = camelCaseWord.charCodeAt(i); + if (isUpper(c) || isNumber(c) || (i > 0 && !isAlphanumeric(camelCaseWord.charCodeAt(i - 1)))) { + return i; + } + } + return camelCaseWord.length; +} + +function _matchesCamelCase(word: string, camelCaseWord: string, i: number, j: number): IMatch[] { + if (i === word.length) { + return []; + } else if (j === camelCaseWord.length) { + return null; + } else if (word[i] !== camelCaseWord[j].toLowerCase()) { + return null; + } else { + let result: IMatch[] = null; + let nextUpperIndex = j + 1; + result = _matchesCamelCase(word, camelCaseWord, i + 1, j + 1); + while (!result && (nextUpperIndex = nextAnchor(camelCaseWord, nextUpperIndex)) < camelCaseWord.length) { + result = _matchesCamelCase(word, camelCaseWord, i + 1, nextUpperIndex); + nextUpperIndex++; + } + return result === null ? null : join({ start: j, end: j + 1 }, result); + } +} + +interface ICamelCaseAnalysis { + upperPercent: number; + lowerPercent: number; + alphaPercent: number; + numericPercent: number; +} + +// Heuristic to avoid computing camel case matcher for words that don't +// look like camelCaseWords. +function analyzeCamelCaseWord(word: string): ICamelCaseAnalysis { + let upper = 0, lower = 0, alpha = 0, numeric = 0, code = 0; + + for (let i = 0; i < word.length; i++) { + code = word.charCodeAt(i); + + if (isUpper(code)) { upper++; } + if (isLower(code)) { lower++; } + if (isAlphanumeric(code)) { alpha++; } + if (isNumber(code)) { numeric++; } + } + + let upperPercent = upper / word.length; + let lowerPercent = lower / word.length; + let alphaPercent = alpha / word.length; + let numericPercent = numeric / word.length; + + return { upperPercent, lowerPercent, alphaPercent, numericPercent }; +} + +function isUpperCaseWord(analysis: ICamelCaseAnalysis): boolean { + const { upperPercent, lowerPercent } = analysis; + return lowerPercent === 0 && upperPercent > 0.6; +} + +function isCamelCaseWord(analysis: ICamelCaseAnalysis): boolean { + const { upperPercent, lowerPercent, alphaPercent, numericPercent } = analysis; + return lowerPercent > 0.2 && upperPercent < 0.8 && alphaPercent > 0.6 && numericPercent < 0.2; +} + +// Heuristic to avoid computing camel case matcher for words that don't +// look like camel case patterns. +function isCamelCasePattern(word: string): boolean { + let upper = 0, lower = 0, code = 0, whitespace = 0; + + for (let i = 0; i < word.length; i++) { + code = word.charCodeAt(i); + + if (isUpper(code)) { upper++; } + if (isLower(code)) { lower++; } + if (isWhitespace(code)) { whitespace++; } + } + + if ((upper === 0 || lower === 0) && whitespace === 0) { + return word.length <= 30; + } else { + return upper <= 5; + } +} + +export function matchesCamelCase(word: string, camelCaseWord: string): IMatch[] { + if (!camelCaseWord) { + return null; + } + + camelCaseWord = camelCaseWord.trim(); + + if (camelCaseWord.length === 0) { + return null; + } + + if (!isCamelCasePattern(word)) { + return null; + } + + if (camelCaseWord.length > 60) { + return null; + } + + const analysis = analyzeCamelCaseWord(camelCaseWord); + + if (!isCamelCaseWord(analysis)) { + if (!isUpperCaseWord(analysis)) { + return null; + } + + camelCaseWord = camelCaseWord.toLowerCase(); + } + + let result: IMatch[] = null; + let i = 0; + + word = word.toLowerCase(); + while (i < camelCaseWord.length && (result = _matchesCamelCase(word, camelCaseWord, 0, i)) === null) { + i = nextAnchor(camelCaseWord, i + 1); + } + + return result; +} + +export function createMatches(position: number[]): IMatch[] { + let ret: IMatch[] = []; + if (!position) { + return ret; + } + let last: IMatch; + for (const pos of position) { + if (last && last.end === pos) { + last.end += 1; + } else { + last = { start: pos, end: pos + 1 }; + ret.push(last); + } + } + return ret; +} diff --git a/extensions/search-rg/src/normalization.ts b/extensions/search-rg/src/common/normalization.ts similarity index 100% rename from extensions/search-rg/src/normalization.ts rename to extensions/search-rg/src/common/normalization.ts diff --git a/extensions/search-rg/src/common/strings.ts b/extensions/search-rg/src/common/strings.ts new file mode 100644 index 00000000000..2678aff1e0f --- /dev/null +++ b/extensions/search-rg/src/common/strings.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { CharCode } from './charCode'; + +export function stripWildcards(pattern: string): string { + return pattern.replace(/\*/g, ''); +} + +/** + * Determines if haystack starts with needle. + */ +export function startsWith(haystack: string, needle: string): boolean { + if (haystack.length < needle.length) { + return false; + } + + if (haystack === needle) { + return true; + } + + for (let i = 0; i < needle.length; i++) { + if (haystack[i] !== needle[i]) { + return false; + } + } + + return true; +} + +export function startsWithIgnoreCase(str: string, candidate: string): boolean { + const candidateLength = candidate.length; + if (candidate.length > str.length) { + return false; + } + + return doEqualsIgnoreCase(str, candidate, candidateLength); +} + +/** + * Determines if haystack ends with needle. + */ +export function endsWith(haystack: string, needle: string): boolean { + let diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.indexOf(needle, diff) === diff; + } else if (diff === 0) { + return haystack === needle; + } else { + return false; + } +} + +function isLowerAsciiLetter(code: number): boolean { + return code >= CharCode.a && code <= CharCode.z; +} + +function isUpperAsciiLetter(code: number): boolean { + return code >= CharCode.A && code <= CharCode.Z; +} + +function isAsciiLetter(code: number): boolean { + return isLowerAsciiLetter(code) || isUpperAsciiLetter(code); +} + +export function equalsIgnoreCase(a: string, b: string): boolean { + const len1 = a ? a.length : 0; + const len2 = b ? b.length : 0; + + if (len1 !== len2) { + return false; + } + + return doEqualsIgnoreCase(a, b); +} + +function doEqualsIgnoreCase(a: string, b: string, stopAt = a.length): boolean { + if (typeof a !== 'string' || typeof b !== 'string') { + return false; + } + + for (let i = 0; i < stopAt; i++) { + const codeA = a.charCodeAt(i); + const codeB = b.charCodeAt(i); + + if (codeA === codeB) { + continue; + } + + // a-z A-Z + if (isAsciiLetter(codeA) && isAsciiLetter(codeB)) { + let diff = Math.abs(codeA - codeB); + if (diff !== 0 && diff !== 32) { + return false; + } + } + + // Any other charcode + else { + if (String.fromCharCode(codeA).toLowerCase() !== String.fromCharCode(codeB).toLowerCase()) { + return false; + } + } + } + + return true; +} + +/** + * Checks if the characters of the provided query string are included in the + * target string. The characters do not have to be contiguous within the string. + */ +export function fuzzyContains(target: string, query: string): boolean { + if (!target || !query) { + return false; // return early if target or query are undefined + } + + if (target.length < query.length) { + return false; // impossible for query to be contained in target + } + + const queryLen = query.length; + const targetLower = target.toLowerCase(); + + let index = 0; + let lastIndexOf = -1; + while (index < queryLen) { + let indexOf = targetLower.indexOf(query[index], lastIndexOf + 1); + if (indexOf < 0) { + return false; + } + + lastIndexOf = indexOf; + + index++; + } + + return true; +} \ No newline at end of file diff --git a/extensions/search-rg/src/extension.ts b/extensions/search-rg/src/extension.ts index a2e4611ffef..f999cb0cb6c 100644 --- a/extensions/search-rg/src/extension.ts +++ b/extensions/search-rg/src/extension.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import { RipgrepTextSearchEngine } from './ripgrepTextSearch'; import { RipgrepFileSearchEngine } from './ripgrepFileSearch'; +import { CachedSearchProvider } from './cachedSearchProvider'; export function activate(): void { if (vscode.workspace.getConfiguration('searchRipgrep').get('enable')) { @@ -15,17 +16,39 @@ export function activate(): void { } } +type SearchEngine = RipgrepFileSearchEngine | RipgrepTextSearchEngine; + class RipgrepSearchProvider implements vscode.SearchProvider { + private cachedProvider: CachedSearchProvider; + private inProgress: Set = new Set(); + constructor(private outputChannel: vscode.OutputChannel) { + this.cachedProvider = new CachedSearchProvider(); + process.once('exit', () => this.dispose()); } provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { const engine = new RipgrepTextSearchEngine(this.outputChannel); - return engine.provideTextSearchResults(query, options, progress, token); + return this.withEngine(engine, () => engine.provideTextSearchResults(query, options, progress, token)); } - provideFileSearchResults(options: vscode.SearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.SearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { const engine = new RipgrepFileSearchEngine(this.outputChannel); - return engine.provideFileSearchResults(options, progress, token); + return this.withEngine(engine, () => this.cachedProvider.provideFileSearchResults(engine, query, options, progress, token)); + } + + clearCache(cacheKey: string): void { + this.cachedProvider.clearCache(cacheKey); + } + + private withEngine(engine: SearchEngine, fn: () => Thenable): Thenable { + this.inProgress.add(engine); + return fn().then(() => { + this.inProgress.delete(engine); + }); + } + + private dispose() { + this.inProgress.forEach(engine => engine.cancel()); } } \ No newline at end of file diff --git a/extensions/search-rg/src/ripgrepFileSearch.ts b/extensions/search-rg/src/ripgrepFileSearch.ts index eaa75d647ef..7edf4c4f4b9 100644 --- a/extensions/search-rg/src/ripgrepFileSearch.ts +++ b/extensions/search-rg/src/ripgrepFileSearch.ts @@ -7,30 +7,31 @@ import * as cp from 'child_process'; import { Readable } from 'stream'; import { NodeStringDecoder, StringDecoder } from 'string_decoder'; import * as vscode from 'vscode'; -import { normalizeNFC, normalizeNFD } from './normalization'; +import { normalizeNFC, normalizeNFD } from './common/normalization'; import { rgPath } from './ripgrep'; -import { anchorGlob } from './ripgrepHelpers'; +import { anchorGlob } from './utils'; import { rgErrorMsgForDisplay } from './ripgrepTextSearch'; +import { IInternalFileSearchProvider } from './cachedSearchProvider'; const isMac = process.platform === 'darwin'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); -export class RipgrepFileSearchEngine { +export class RipgrepFileSearchEngine implements IInternalFileSearchProvider { private rgProc: cp.ChildProcess; - private killRgProcFn: (code?: number) => void; + private isDone: boolean; - constructor(private outputChannel: vscode.OutputChannel) { - this.killRgProcFn = () => this.rgProc && this.rgProc.kill(); - process.once('exit', this.killRgProcFn); + constructor(private outputChannel: vscode.OutputChannel) { } + + cancel() { + this.isDone = true; + if (this.rgProc) { + this.rgProc.kill(); + } } - private dispose() { - process.removeListener('exit', this.killRgProcFn); - } - - provideFileSearchResults(options: vscode.SearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { this.outputChannel.appendLine(`provideFileSearchResults ${JSON.stringify({ ...options, ...{ @@ -39,12 +40,7 @@ export class RipgrepFileSearchEngine { })}`); return new Promise((resolve, reject) => { - let isDone = false; - const cancel = () => { - isDone = true; - this.rgProc.kill(); - }; - token.onCancellationRequested(cancel); + token.onCancellationRequested(() => this.cancel()); const rgArgs = getRgArgs(options); @@ -93,7 +89,7 @@ export class RipgrepFileSearchEngine { }); if (last) { - if (isDone) { + if (this.isDone) { resolve(); } else { // Trigger last result @@ -106,12 +102,7 @@ export class RipgrepFileSearchEngine { } } }); - }).then( - () => this.dispose(), - err => { - this.dispose(); - return Promise.reject(err); - }); + }); } private collectStdout(cmd: cp.ChildProcess, cb: (err: Error, stdout?: string, last?: boolean) => void): void { diff --git a/extensions/search-rg/src/ripgrepTextSearch.ts b/extensions/search-rg/src/ripgrepTextSearch.ts index b03ee60c754..728365bff17 100644 --- a/extensions/search-rg/src/ripgrepTextSearch.ts +++ b/extensions/search-rg/src/ripgrepTextSearch.ts @@ -7,10 +7,11 @@ import * as cp from 'child_process'; import { EventEmitter } from 'events'; +import * as path from 'path'; import { NodeStringDecoder, StringDecoder } from 'string_decoder'; import * as vscode from 'vscode'; import { rgPath } from './ripgrep'; -import { anchorGlob } from './ripgrepHelpers'; +import { anchorGlob } from './utils'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); @@ -21,12 +22,21 @@ const MAX_TEXT_RESULTS = 10000; export class RipgrepTextSearchEngine { private isDone = false; private rgProc: cp.ChildProcess; - private killRgProcFn: (code?: number) => void; private ripgrepParser: RipgrepParser; - constructor(private outputChannel: vscode.OutputChannel) { - this.killRgProcFn = () => this.rgProc && this.rgProc.kill(); + constructor(private outputChannel: vscode.OutputChannel) { } + + cancel() { + this.isDone = true; + + if (this.rgProc) { + this.rgProc.kill(); + } + + if (this.ripgrepParser) { + this.ripgrepParser.cancel(); + } } provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { @@ -38,12 +48,7 @@ export class RipgrepTextSearchEngine { })}`); return new Promise((resolve, reject) => { - const cancel = () => { - this.isDone = true; - this.ripgrepParser.cancel(); - this.rgProc.kill(); - }; - token.onCancellationRequested(cancel); + token.onCancellationRequested(() => this.cancel()); const rgArgs = getRgArgs(query, options); @@ -55,7 +60,6 @@ export class RipgrepTextSearchEngine { this.outputChannel.appendLine(`rg ${escapedArgs}\n - cwd: ${cwd}`); this.rgProc = cp.spawn(rgDiskPath, rgArgs, { cwd }); - process.once('exit', this.killRgProcFn); this.rgProc.on('error', e => { console.error(e); this.outputChannel.append('Error: ' + (e && e.message)); @@ -70,7 +74,7 @@ export class RipgrepTextSearchEngine { }); this.ripgrepParser.on('hitLimit', () => { - cancel(); + this.cancel(); }); this.rgProc.stdout.on('data', data => { @@ -91,7 +95,6 @@ export class RipgrepTextSearchEngine { this.outputChannel.appendLine(gotData ? 'Got data from stdout' : 'No data from stdout'); this.outputChannel.appendLine(gotResult ? 'Got result from parser' : 'No result from parser'); this.outputChannel.appendLine(''); - process.removeListener('exit', this.killRgProcFn); if (this.isDone) { resolve(); } else { @@ -291,7 +294,7 @@ export class RipgrepParser extends EventEmitter { lineMatches .map(range => { return { - path: this.currentFile, + uri: vscode.Uri.file(path.join(this.rootFolder, this.currentFile)), range, preview: { text: preview, diff --git a/extensions/search-rg/src/ripgrepHelpers.ts b/extensions/search-rg/src/utils.ts similarity index 78% rename from extensions/search-rg/src/ripgrepHelpers.ts rename to extensions/search-rg/src/utils.ts index 5487be750a0..fd083fe075f 100644 --- a/extensions/search-rg/src/ripgrepHelpers.ts +++ b/extensions/search-rg/src/utils.ts @@ -5,9 +5,8 @@ 'use strict'; -import * as vscode from 'vscode'; - import * as path from 'path'; +import * as vscode from 'vscode'; export function fixDriveC(_path: string): string { const root = path.parse(_path).root; @@ -19,3 +18,10 @@ export function fixDriveC(_path: string): string { export function anchorGlob(glob: string): string { return glob.startsWith('**') || glob.startsWith('/') ? glob : `/${glob}`; } + +export function joinPath(resource: vscode.Uri, pathFragment: string): vscode.Uri { + const joinedPath = path.join(resource.path || '/', pathFragment); + return resource.with({ + path: joinedPath + }); +} diff --git a/extensions/search-rg/yarn.lock b/extensions/search-rg/yarn.lock index 4eaab0b7bb3..99b491f01ff 100644 --- a/extensions/search-rg/yarn.lock +++ b/extensions/search-rg/yarn.lock @@ -1545,9 +1545,9 @@ vscode-extension-telemetry@0.0.15: dependencies: applicationinsights "1.0.1" -vscode-nls@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" vscode-ripgrep@^1.0.1: version "1.0.1" diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index c56e185f844..bee4c021973 100644 --- a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/7bf8960f7042474b10b519f39339fc527907ce16", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/40288b872220e5c0b844b1de507f1749ed14589b", "name": "TypeScript", "scopeName": "source.ts", "patterns": [ @@ -4149,7 +4149,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.ts", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.ts" @@ -4177,7 +4177,7 @@ "patterns": [ { "name": "entity.other.attribute-name.directive.ts", - "match": "path|types|no-default-lib|name" + "match": "path|types|no-default-lib|lib|name" }, { "name": "keyword.operator.assignment.ts", diff --git a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json index 8ef6980ec42..441f07d6abd 100644 --- a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/7bf8960f7042474b10b519f39339fc527907ce16", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/88217b1c7d36ed5d35adc3099ba7978aabe2531b", "name": "TypeScriptReact", "scopeName": "source.tsx", "patterns": [ @@ -4115,7 +4115,7 @@ }, "directives": { "name": "comment.line.triple-slash.directive.tsx", - "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", + "begin": "^(///)\\s*(?=<(reference|amd-dependency|amd-module)(\\s+(path|types|no-default-lib|lib|name)\\s*=\\s*((\\'([^\\'\\\\]|\\\\\\'|\\\\)*\\')|(\\\"([^\\\"\\\\]|\\\\\\\"|\\\\)*\\\")))+\\s*/>\\s*$)", "beginCaptures": { "1": { "name": "punctuation.definition.comment.tsx" @@ -4143,7 +4143,7 @@ "patterns": [ { "name": "entity.other.attribute-name.directive.tsx", - "match": "path|types|no-default-lib|name" + "match": "path|types|no-default-lib|lib|name" }, { "name": "keyword.operator.assignment.tsx", @@ -4664,8 +4664,8 @@ ] }, "jsx-tag-in-expression": { - "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", - "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", + "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", + "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "patterns": [ { "include": "#jsx-tag" @@ -4674,7 +4674,7 @@ }, "jsx-tag": { "name": "meta.tag.tsx", - "begin": "(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>))", + "begin": "(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "end": "(/>)|(?:())", "endCaptures": { "1": { @@ -4701,7 +4701,7 @@ }, "patterns": [ { - "begin": "(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>|\\<\\s*(((keyof|infer)\\s+)|(([_$[:alpha:]][_$[:alnum:]]*|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\'[^\\']*\\')|(\\\"[^\\\"]*\\\")|(\\`[^\\`]*\\`))(?=\\s*([\\<\\>\\,\\.\\[=]|&(?!&)|\\|(?!\\|)))))([^<>\\(]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(?<==)\\>)*(?!=)\\>)*(?!=)>\\s*)|(\\s+))(?!\\?)|\\/?>)", + "begin": "(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?)", "beginCaptures": { "1": { "name": "punctuation.definition.tag.begin.tsx" diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index d94a54d8837..841024fd02b 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -18,7 +18,7 @@ "dependencies": { "semver": "4.3.6", "vscode-extension-telemetry": "0.0.17", - "vscode-nls": "^3.2.1" + "vscode-nls": "^3.2.4" }, "devDependencies": { "@types/node": "8.0.33", diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index c7ce7bf6e56..2c4ece54764 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -318,20 +318,9 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider } const enableDotCompletions = this.shouldEnableDotCompletions(document, position); - - const completionItems: vscode.CompletionItem[] = []; - for (const element of msg) { - if (element.kind === PConst.Kind.warning && !completionConfiguration.nameSuggestions) { - continue; - } - if (!completionConfiguration.autoImportSuggestions && element.hasAction) { - continue; - } - const item = new MyCompletionItem(position, document, line.text, element, enableDotCompletions, completionConfiguration.useCodeSnippetsOnMethodSuggest); - completionItems.push(item); - } - - return completionItems; + return msg + .filter(entry => !shouldExcludeCompletionEntry(entry, completionConfiguration)) + .map(entry => new MyCompletionItem(position, document, line.text, entry, enableDotCompletions, completionConfiguration.useCodeSnippetsOnMethodSuggest)); } public async resolveCompletionItem( @@ -595,6 +584,17 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider } } +function shouldExcludeCompletionEntry( + element: Proto.CompletionEntry, + completionConfiguration: CompletionConfiguration +) { + return ( + (!completionConfiguration.nameSuggestions && element.kind === PConst.Kind.warning) + || (!completionConfiguration.quickSuggestionsForPaths && + (element.kind === PConst.Kind.directory || element.kind === PConst.Kind.script)) + || (!completionConfiguration.autoImportSuggestions && element.hasAction) + ); +} export function register( selector: vscode.DocumentSelector, diff --git a/extensions/typescript-language-features/src/test/previewer.test.ts b/extensions/typescript-language-features/src/test/previewer.test.ts new file mode 100644 index 00000000000..011187b2af4 --- /dev/null +++ b/extensions/typescript-language-features/src/test/previewer.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import { tagsMarkdownPreview } from '../utils/previewer'; + +suite('typescript.previewer', () => { + test('Should ignore hyphens after a param tag', async () => { + assert.strictEqual( + tagsMarkdownPreview([ + { + name: 'param', + text: 'a - b' + } + ]), + '*@param* `a` — b'); + }); +}); + diff --git a/extensions/typescript-language-features/src/utils/previewer.ts b/extensions/typescript-language-features/src/utils/previewer.ts index 647ae3bf91b..ce10fcbbe04 100644 --- a/extensions/typescript-language-features/src/utils/previewer.ts +++ b/extensions/typescript-language-features/src/utils/previewer.ts @@ -27,7 +27,7 @@ function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined { function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { switch (tag.name) { case 'param': - const body = (tag.text || '').split(/^([\w\.]+)\s*/); + const body = (tag.text || '').split(/^([\w\.]+)\s*-?\s*/); if (body && body.length === 3) { const param = body[1]; const doc = body[2]; @@ -81,4 +81,4 @@ export function addMarkdownDocumentation( out.appendMarkdown('\n\n' + tagsPreview); } return out; -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index b9c4b1eece7..39422f56063 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -1548,9 +1548,9 @@ vscode-extension-telemetry@0.0.17: dependencies: applicationinsights "1.0.1" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398" vscode@^1.1.10: version "1.1.17" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 1f42a34e597..2e9fe0d0bbf 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -511,6 +511,16 @@ suite('workspace-namespace', () => { // }); // }); + test('findTextInFiles', async () => { + const results: vscode.TextSearchResult[] = []; + await vscode.workspace.findTextInFiles({ pattern: 'foo' }, { include: '*.ts' }, result => { + results.push(result); + }); + + assert.equal(results.length, 1); + assert.equal(vscode.workspace.asRelativePath(results[0].uri), '10linefile.ts'); + }); + test('applyEdit', () => { return vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:' + join(vscode.workspace.rootPath || '', './new2.txt'))).then(doc => { @@ -603,8 +613,8 @@ suite('workspace-namespace', () => { let newDoc = await vscode.workspace.openTextDocument(newUri); assert.equal(newDoc.getText(), 'HelloFoo'); - // let doc = await vscode.workspace.openTextDocument(docUri); - // assert.equal(doc.getText(), 'Bar'); + let doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.getText(), 'Bar'); }); test('WorkspaceEdit api - after saving a deleted file, it still shows up as deleted. #42667', async function () { @@ -679,8 +689,7 @@ suite('workspace-namespace', () => { we = new vscode.WorkspaceEdit(); we.createFile(docUri, { overwrite: true }); assert.ok(await vscode.workspace.applyEdit(we)); - // todo@ben - // assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), ''); + assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), ''); }); test('WorkspaceEdit: create & ignoreIfExists', async function () { diff --git a/package.json b/package.json index 4bd43946cd7..8e586f38e03 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "vscode-nsfw": "1.0.17", "vscode-ripgrep": "^1.0.1", "vscode-textmate": "^4.0.1", - "vscode-xterm": "3.5.0-beta17", + "vscode-xterm": "3.6.0-beta2", "yauzl": "^2.9.1" }, "devDependencies": { diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index f0015d389d3..98bc02c22cd 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -29,6 +29,8 @@ function readFile(file) { }); } +const writeFile = (file, content) => new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); + var rawNlsConfig = process.env['VSCODE_NLS_CONFIG']; var nlsConfig = rawNlsConfig ? JSON.parse(rawNlsConfig) : { availableLanguages: {} }; @@ -46,8 +48,15 @@ if (nlsConfig._resolvedLanguagePackCoreLocation) { let json = JSON.parse(content); bundles[bundle] = json; cb(undefined, json); - }) - .catch(cb); + }).catch((error) => { + try { + if (nlsConfig._corruptedFile) { + writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + } + } finally { + cb(error, undefined); + } + }); }; } diff --git a/src/main.js b/src/main.js index e36fda1228d..ca00474ba1e 100644 --- a/src/main.js +++ b/src/main.js @@ -100,6 +100,19 @@ const args = minimist(process.argv, { ] }); +function getUserDataPath() { + if (isPortable) { + return path.join(portableDataPath, 'user-data'); + } + + return path.resolve(args['user-data-dir'] || paths.getDefaultUserDataPath(process.platform)); +} + +const userDataPath = getUserDataPath(); + +// Set userData path before app 'ready' event and call to process.chdir +app.setPath('userData', userDataPath); + //#region NLS function stripComments(content) { let regexp = /("(?:[^\\\"]*(?:\\.)?)*")|('(?:[^\\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; @@ -130,6 +143,10 @@ const exists = file => new Promise(c => fs.exists(file, c)); const readFile = file => new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data))); const writeFile = (file, content) => new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); const touch = file => new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); }); +const lstat = file => new Promise((c, e) => fs.lstat(file, (err, stats) => err ? e(err) : c(stats))); +const readdir = dir => new Promise((c, e) => fs.readdir(dir, (err, files) => err ? e(err) : c(files))); +const rmdir = dir => new Promise((c, e) => fs.rmdir(dir, err => err ? e(err) : c(undefined))); +const unlink = file => new Promise((c, e) => fs.unlink(file, err => err ? e(err) : c(undefined))); function mkdirp(dir) { return mkdir(dir).then(null, err => { @@ -145,6 +162,23 @@ function mkdirp(dir) { }); } +function rimraf(location) { + return lstat(location).then(stat => { + if (stat.isDirectory() && !stat.isSymbolicLink()) { + return readdir(location) + .then(children => Promise.all(children.map(child => rimraf(path.join(location, child))))) + .then(() => rmdir(location)); + } else { + return unlink(location); + } + }, (err) => { + if (err.code === 'ENOENT') { + return void 0; + } + throw err; + }); +} + function resolveJSFlags(...jsFlags) { if (args['js-flags']) { @@ -169,8 +203,7 @@ function getUserDefinedLocale() { return Promise.resolve(locale.toLowerCase()); } - let userData = app.getPath('userData'); - let localeConfig = path.join(userData, 'User', 'locale.json'); + let localeConfig = path.join(userDataPath, 'User', 'locale.json'); return exists(localeConfig).then((result) => { if (result) { return readFile(localeConfig).then((content) => { @@ -189,8 +222,7 @@ function getUserDefinedLocale() { } function getLanguagePackConfigurations() { - let userData = app.getPath('userData'); - let configFile = path.join(userData, 'languagepacks.json'); + let configFile = path.join(userDataPath, 'languagepacks.json'); try { return require(configFile); } catch (err) { @@ -229,8 +261,6 @@ function getNLSConfiguration(locale) { return Promise.resolve({ locale: locale, availableLanguages: {} }); } - let userData = app.getPath('userData'); - // We have a built version so we have extracted nls file. Try to find // the right file to use. @@ -271,65 +301,78 @@ function getNLSConfiguration(locale) { return defaultResult(initialLocale); } let packId = packConfig.hash + '.' + locale; - let cacheRoot = path.join(userData, 'clp', packId); + let cacheRoot = path.join(userDataPath, 'clp', packId); let coreLocation = path.join(cacheRoot, commit); let translationsConfigFile = path.join(cacheRoot, 'tcf.json'); + let corruptedFile = path.join(cacheRoot, 'corrupted.info'); let result = { locale: initialLocale, availableLanguages: { '*': locale }, _languagePackId: packId, _translationsConfigFile: translationsConfigFile, _cacheRoot: cacheRoot, - _resolvedLanguagePackCoreLocation: coreLocation + _resolvedLanguagePackCoreLocation: coreLocation, + _corruptedFile: corruptedFile }; - return exists(coreLocation).then((fileExists) => { - if (fileExists) { - // We don't wait for this. No big harm if we can't touch - touch(coreLocation).catch(() => { }); - perf.mark('nlsGeneration:end'); - return result; + return exists(corruptedFile).then((corrupted) => { + // The nls cache directory is corrupted. + let toDelete; + if (corrupted) { + toDelete = rimraf(cacheRoot); + } else { + toDelete = Promise.resolve(undefined); } - return mkdirp(coreLocation).then(() => { - return Promise.all([readFile(path.join(__dirname, 'nls.metadata.json')), readFile(mainPack)]); - }).then((values) => { - let metadata = JSON.parse(values[0]); - let packData = JSON.parse(values[1]).contents; - let bundles = Object.keys(metadata.bundles); - let writes = []; - for (let bundle of bundles) { - let modules = metadata.bundles[bundle]; - let target = Object.create(null); - for (let module of modules) { - let keys = metadata.keys[module]; - let defaultMessages = metadata.messages[module]; - let translations = packData[module]; - let targetStrings; - if (translations) { - targetStrings = []; - for (let i = 0; i < keys.length; i++) { - let elem = keys[i]; - let key = typeof elem === 'string' ? elem : elem.key; - let translatedMessage = translations[key]; - if (translatedMessage === undefined) { - translatedMessage = defaultMessages[i]; - } - targetStrings.push(translatedMessage); - } - } else { - targetStrings = defaultMessages; - } - target[module] = targetStrings; + return toDelete.then(() => { + return exists(coreLocation).then((fileExists) => { + if (fileExists) { + // We don't wait for this. No big harm if we can't touch + touch(coreLocation).catch(() => { }); + perf.mark('nlsGeneration:end'); + return result; } - writes.push(writeFile(path.join(coreLocation, bundle.replace(/\//g, '!') + '.nls.json'), JSON.stringify(target))); - } - writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations))); - return Promise.all(writes); - }).then(() => { - perf.mark('nlsGeneration:end'); - return result; - }).catch((err) => { - console.error('Generating translation files failed.', err); - return defaultResult(locale); + return mkdirp(coreLocation).then(() => { + return Promise.all([readFile(path.join(__dirname, 'nls.metadata.json')), readFile(mainPack)]); + }).then((values) => { + let metadata = JSON.parse(values[0]); + let packData = JSON.parse(values[1]).contents; + let bundles = Object.keys(metadata.bundles); + let writes = []; + for (let bundle of bundles) { + let modules = metadata.bundles[bundle]; + let target = Object.create(null); + for (let module of modules) { + let keys = metadata.keys[module]; + let defaultMessages = metadata.messages[module]; + let translations = packData[module]; + let targetStrings; + if (translations) { + targetStrings = []; + for (let i = 0; i < keys.length; i++) { + let elem = keys[i]; + let key = typeof elem === 'string' ? elem : elem.key; + let translatedMessage = translations[key]; + if (translatedMessage === undefined) { + translatedMessage = defaultMessages[i]; + } + targetStrings.push(translatedMessage); + } + } else { + targetStrings = defaultMessages; + } + target[module] = targetStrings; + } + writes.push(writeFile(path.join(coreLocation, bundle.replace(/\//g, '!') + '.nls.json'), JSON.stringify(target))); + } + writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations))); + return Promise.all(writes); + }).then(() => { + perf.mark('nlsGeneration:end'); + return result; + }).catch((err) => { + console.error('Generating translation files failed.', err); + return defaultResult(locale); + }); + }); }); }); }); @@ -368,23 +411,12 @@ const nodeCachedDataDir = new class { if (!commit) { return undefined; } - return path.join(app.getPath('userData'), 'CachedData', commit); + return path.join(userDataPath, 'CachedData', commit); } }; //#endregion -function getUserDataPath() { - if (isPortable) { - return path.join(portableDataPath, 'user-data'); - } - - return path.resolve(args['user-data-dir'] || paths.getDefaultUserDataPath(process.platform)); -} - -// Set userData path before app 'ready' event and call to process.chdir -app.setPath('userData', getUserDataPath()); - // Update cwd based on environment and platform try { if (process.platform === 'win32') { diff --git a/src/tsconfig.json b/src/tsconfig.json index d2b9e229c4a..408cad4917b 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -18,5 +18,10 @@ "typeRoots": [ "typings" ] - } + }, + "exclude": [ + "../out", + "../out-build", + "../out-vscode" + ] } diff --git a/src/typings/vscode-xterm.d.ts b/src/typings/vscode-xterm.d.ts index 8661eec2dd7..eb8c87e277a 100644 --- a/src/typings/vscode-xterm.d.ts +++ b/src/typings/vscode-xterm.d.ts @@ -124,6 +124,15 @@ declare module 'vscode-xterm' { */ macOptionIsMeta?: boolean; + /** + * Whether holding a modifier key will force normal selection behavior, + * regardless of whether the terminal is in mouse events mode. This will + * also prevent mouse events from being emitted by the terminal. For example, + * this allows you to use xterm.js' regular selection inside tmux with + * mouse mode enabled. + */ + macOptionClickForcesSelection?: boolean; + /** * (EXPERIMENTAL) The type of renderer to use, this allows using the * fallback DOM renderer when canvas is too slow for the environment. The diff --git a/src/vs/base/browser/builder.ts b/src/vs/base/browser/builder.ts index 74b574df9a0..9c285274a99 100644 --- a/src/vs/base/browser/builder.ts +++ b/src/vs/base/browser/builder.ts @@ -5,9 +5,8 @@ 'use strict'; import 'vs/css!./builder'; -import { TPromise } from 'vs/base/common/winjs.base'; import * as types from 'vs/base/common/types'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import * as assert from 'vs/base/common/assert'; import * as DOM from 'vs/base/browser/dom'; @@ -907,7 +906,7 @@ export class Builder implements IDisposable { this.attr('aria-hidden', 'false'); // Cancel any pending showDelayed() invocation - this.cancelVisibilityPromise(); + this.cancelVisibilityTimeout(); return this; } @@ -922,15 +921,15 @@ export class Builder implements IDisposable { showDelayed(delay: number): Builder { // Cancel any pending showDelayed() invocation - this.cancelVisibilityPromise(); + this.cancelVisibilityTimeout(); - let promise = TPromise.timeout(delay); - this.setProperty(VISIBILITY_BINDING_ID, promise); - - promise.done(() => { + // Install new delay for showing + const handle = setTimeout(() => { this.removeProperty(VISIBILITY_BINDING_ID); this.show(); - }); + }, delay); + + this.setProperty(VISIBILITY_BINDING_ID, toDisposable(() => clearTimeout(handle))); return this; } @@ -945,7 +944,7 @@ export class Builder implements IDisposable { this.attr('aria-hidden', 'true'); // Cancel any pending showDelayed() invocation - this.cancelVisibilityPromise(); + this.cancelVisibilityTimeout(); return this; } @@ -957,10 +956,10 @@ export class Builder implements IDisposable { return this.hasClass('monaco-builder-hidden') || this.currentElement.style.display === 'none'; } - private cancelVisibilityPromise(): void { - let promise: TPromise = this.getProperty(VISIBILITY_BINDING_ID); - if (promise) { - promise.cancel(); + private cancelVisibilityTimeout(): void { + const visibilityDisposable = this.getProperty(VISIBILITY_BINDING_ID) as IDisposable; + if (visibilityDisposable) { + visibilityDisposable.dispose(); this.removeProperty(VISIBILITY_BINDING_ID); } } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 6e7c479e58d..02b3d4115ea 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -819,6 +819,7 @@ export const EventType = { MOUSE_OVER: 'mouseover', MOUSE_MOVE: 'mousemove', MOUSE_OUT: 'mouseout', + MOUSE_ENTER: 'mouseenter', MOUSE_LEAVE: 'mouseleave', CONTEXT_MENU: 'contextmenu', WHEEL: 'wheel', diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css new file mode 100644 index 00000000000..a7f820dee40 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-breadcrumbs { + user-select: none; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; +} + +.monaco-breadcrumbs .monaco-breadcrumb-item { + display: flex; + align-items: center; + flex: 0 1 auto; + white-space: nowrap; + cursor: default; + align-self: center; + height: 100%; +} + +.monaco-breadcrumbs .monaco-breadcrumb-item:not(:last-child)::after { + background-image: url(./collapsed.svg); + width: 16px; + height: 16px; + display: inline-block; + background-size: 16px; + background-position: 50% 50%; + content: ' '; +} + +.vs-dark .monaco-breadcrumbs .monaco-breadcrumb-item:not(:last-child)::after { + background-image: url(./collpased-dark.svg); +} + +.monaco-breadcrumbs .monaco-breadcrumb-item.focused { + font-weight: bold; +} diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts new file mode 100644 index 00000000000..29cee352905 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./breadcrumbsWidget'; +import * as dom from 'vs/base/browser/dom'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Color } from 'vs/base/common/color'; +import { commonPrefixLength, tail } from 'vs/base/common/arrays'; + +export abstract class BreadcrumbsItem { + dispose(): void { } + abstract equals(other: BreadcrumbsItem): boolean; + abstract render(container: HTMLElement): void; +} + +export class SimpleBreadcrumbsItem extends BreadcrumbsItem { + + constructor( + readonly text: string, + readonly title: string = text + ) { + super(); + } + + equals(other: this) { + return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title; + } + + render(container: HTMLElement): void { + let node = document.createElement('div'); + node.title = this.title; + node.innerText = this.text; + container.appendChild(node); + } +} + +export interface IBreadcrumbsWidgetStyles { + breadcrumbsBackground?: Color; + breadcrumbsActiveForeground?: Color; + breadcrumbsInactiveForeground?: Color; +} + +export interface IBreadcrumbsItemEvent { + item: BreadcrumbsItem; + node: HTMLElement; +} + +export class BreadcrumbsWidget { + + private readonly _disposables = new Array(); + private readonly _domNode: HTMLDivElement; + private readonly _styleElement: HTMLStyleElement; + private readonly _scrollable: DomScrollableElement; + + private readonly _onDidSelectItem = new Emitter(); + private readonly _onDidFocusItem = new Emitter(); + private readonly _onDidChangeFocus = new Emitter(); + + readonly onDidSelectItem: Event = this._onDidSelectItem.event; + readonly onDidFocusItem: Event = this._onDidFocusItem.event; + readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; + + private readonly _items = new Array(); + private readonly _nodes = new Array(); + private readonly _freeNodes = new Array(); + + private _focusedItemIdx: number = -1; + private _selectedItemIdx: number = -1; + + constructor( + container: HTMLElement + ) { + this._domNode = document.createElement('div'); + this._domNode.className = 'monaco-breadcrumbs'; + this._domNode.tabIndex = -1; + this._scrollable = new DomScrollableElement(this._domNode, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Auto, + horizontalScrollbarSize: 3, + useShadows: false + }); + this._disposables.push(this._scrollable); + this._disposables.push(dom.addStandardDisposableListener(this._domNode, 'click', e => this._onClick(e))); + container.appendChild(this._scrollable.getDomNode()); + + this._styleElement = dom.createStyleSheet(this._domNode); + + let focusTracker = dom.trackFocus(this._domNode); + this._disposables.push(focusTracker); + this._disposables.push(focusTracker.onDidBlur(_ => this._onDidChangeFocus.fire(false))); + this._disposables.push(focusTracker.onDidFocus(_ => this._onDidChangeFocus.fire(true))); + } + + dispose(): void { + dispose(this._disposables); + this._domNode.remove(); + this._disposables.length = 0; + this._nodes.length = 0; + this._freeNodes.length = 0; + } + + layout(dim: dom.Dimension): void { + if (!dim) { + this._scrollable.scanDomNode(); + } else { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._scrollable.scanDomNode(); + } + } + + style(style: IBreadcrumbsWidgetStyles): void { + let content = ''; + if (style.breadcrumbsBackground) { + content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`; + } + if (style.breadcrumbsActiveForeground) { + content += `.monaco-breadcrumbs:focus .monaco-breadcrumb-item { color: ${style.breadcrumbsActiveForeground}}\n`; + } + if (style.breadcrumbsInactiveForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsInactiveForeground}}\n`; + } + if (this._styleElement.innerHTML !== content) { + this._styleElement.innerHTML = content; + } + } + + domFocus(): void { + const focused = this.getFocused() || tail(this._items); + this.setFocused(focused); + this._domNode.focus(); + } + + getFocused(): BreadcrumbsItem { + return this._items[this._focusedItemIdx]; + } + + setFocused(item: BreadcrumbsItem): void { + this._focus(this._items.indexOf(item)); + } + + focusPrev(): any { + this._focus((this._focusedItemIdx - 1 + this._nodes.length) % this._nodes.length); + this._domNode.focus(); + } + + focusNext(): any { + this._focus((this._focusedItemIdx + 1) % this._nodes.length); + this._domNode.focus(); + } + + private _focus(nth: number): void { + this._focusedItemIdx = -1; + for (let i = 0; i < this._nodes.length; i++) { + const node = this._nodes[i]; + if (i !== nth) { + dom.removeClass(node, 'focused'); + } else { + this._focusedItemIdx = i; + dom.addClass(node, 'focused'); + } + } + this._onDidFocusItem.fire({ item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx] }); + } + + getSelected(): BreadcrumbsItem { + return this._items[this._selectedItemIdx]; + } + + setSelected(item: BreadcrumbsItem): void { + this._select(this._items.indexOf(item)); + } + + private _select(nth: number): void { + this._selectedItemIdx = -1; + for (let i = 0; i < this._nodes.length; i++) { + const node = this._nodes[i]; + if (i !== nth) { + dom.removeClass(node, 'selected'); + } else { + this._selectedItemIdx = i; + dom.addClass(node, 'selected'); + } + } + this._onDidSelectItem.fire({ item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx] }); + } + + setItems(items: BreadcrumbsItem[]): void { + let prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b)); + let removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix)); + this._render(prefix); + dispose(removed); + if (prefix >= this._focusedItemIdx) { + this._focus(-1); + } + } + + private _render(start: number): void { + for (; start < this._items.length && start < this._nodes.length; start++) { + let item = this._items[start]; + let node = this._nodes[start]; + this._renderItem(item, node); + } + // case a: more nodes -> remove them + for (; start < this._nodes.length; start++) { + this._nodes[start].remove(); + this._freeNodes.push(this._nodes[start]); + } + this._nodes.length = this._items.length; + + // case b: more items -> render them + for (; start < this._items.length; start++) { + let item = this._items[start]; + let node = this._freeNodes.length > 0 ? this._freeNodes.pop() : document.createElement('div'); + this._renderItem(item, node); + this._domNode.appendChild(node); + this._nodes[start] = node; + } + this.layout(undefined); + } + + private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void { + dom.clearNode(container); + container.className = ''; + item.render(container); + dom.append(container); + dom.addClass(container, 'monaco-breadcrumb-item'); + } + + private _onClick(event: IMouseEvent): void { + for (let el = event.target; el; el = el.parentElement) { + let idx = this._nodes.indexOf(el as any); + if (idx >= 0) { + this._focus(idx); + this._select(idx); + break; + } + } + } +} diff --git a/src/vs/base/browser/ui/breadcrumbs/collapsed.svg b/src/vs/base/browser/ui/breadcrumbs/collapsed.svg new file mode 100755 index 00000000000..3a63808c358 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/base/browser/ui/breadcrumbs/collpased-dark.svg b/src/vs/base/browser/ui/breadcrumbs/collpased-dark.svg new file mode 100755 index 00000000000..cf5c3641aa7 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/collpased-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/base/browser/ui/countBadge/countBadge.css b/src/vs/base/browser/ui/countBadge/countBadge.css index e6f36db1adc..7429322f3e6 100644 --- a/src/vs/base/browser/ui/countBadge/countBadge.css +++ b/src/vs/base/browser/ui/countBadge/countBadge.css @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ .monaco-count-badge { - padding: 0.2em 0.5em; + padding: 0.3em 0.5em; border-radius: 1em; font-size: 85%; + min-width: 1.6em; + line-height: 1em; font-weight: normal; text-align: center; - display: inline; + display: inline-block; + box-sizing: border-box; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 15e572c3030..649ca14073d 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -279,10 +279,15 @@ export class Grid implements IDisposable { getViewSize(view: T): number { const location = this.getViewLocation(view); const viewSize = this.gridview.getViewSize(location); - return getLocationOrientation(this.orientation, location) === Orientation.HORIZONTAL ? viewSize.width : viewSize.height; } + // TODO@joao cleanup + getViewSize2(view: T): { width: number; height: number; } { + const location = this.getViewLocation(view); + return this.gridview.getViewSize(location); + } + maximizeViewSize(view: T): void { const location = this.getViewLocation(view); this.gridview.maximizeViewSize(location); diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 12619fee81c..98a9d0ef79c 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -9,7 +9,7 @@ import 'vs/css!./gridview'; import { Event, anyEvent, Emitter, mapEvent, Relay } from 'vs/base/common/event'; import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { SplitView, IView as ISplitView, Sizing, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview'; -import { empty as EmptyDisposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { $ } from 'vs/base/browser/dom'; import { tail2 as tail } from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; @@ -122,12 +122,12 @@ class BranchNode implements ISplitView, IDisposable { private _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; - private childrenChangeDisposable: IDisposable = EmptyDisposable; + private childrenChangeDisposable: IDisposable = Disposable.None; private _onDidSashReset = new Emitter(); readonly onDidSashReset: Event = this._onDidSashReset.event; - private splitviewSashResetDisposable: IDisposable = EmptyDisposable; - private childrenSashResetDisposable: IDisposable = EmptyDisposable; + private splitviewSashResetDisposable: IDisposable = Disposable.None; + private childrenSashResetDisposable: IDisposable = Disposable.None; get orthogonalStartSash(): Sash | undefined { return this.splitview.orthogonalStartSash; } set orthogonalStartSash(sash: Sash | undefined) { this.splitview.orthogonalStartSash = sash; } @@ -280,22 +280,22 @@ class BranchNode implements ISplitView, IDisposable { trySet2x2(other: BranchNode): IDisposable { if (this.children.length !== 2 || other.children.length !== 2) { - return EmptyDisposable; + return Disposable.None; } if (this.getChildSize(0) !== other.getChildSize(0)) { - return EmptyDisposable; + return Disposable.None; } const [firstChild, secondChild] = this.children; const [otherFirstChild, otherSecondChild] = other.children; if (!(firstChild instanceof LeafNode) || !(secondChild instanceof LeafNode)) { - return EmptyDisposable; + return Disposable.None; } if (!(otherFirstChild instanceof LeafNode) || !(otherSecondChild instanceof LeafNode)) { - return EmptyDisposable; + return Disposable.None; } if (this.orientation === Orientation.VERTICAL) { @@ -485,7 +485,7 @@ export class GridView implements IDisposable { private onDidSashResetRelay = new Relay(); readonly onDidSashReset: Event = this.onDidSashResetRelay.event; - private disposable2x2: IDisposable = EmptyDisposable; + private disposable2x2: IDisposable = Disposable.None; private get root(): BranchNode { return this._root; @@ -550,7 +550,7 @@ export class GridView implements IDisposable { addView(view: IView, size: number | Sizing, location: number[]): void { this.disposable2x2.dispose(); - this.disposable2x2 = EmptyDisposable; + this.disposable2x2 = Disposable.None; const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); @@ -582,7 +582,7 @@ export class GridView implements IDisposable { removeView(location: number[], sizing?: Sizing): IView { this.disposable2x2.dispose(); - this.disposable2x2 = EmptyDisposable; + this.disposable2x2 = Disposable.None; const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); @@ -783,7 +783,7 @@ export class GridView implements IDisposable { trySet2x2(): void { this.disposable2x2.dispose(); - this.disposable2x2 = EmptyDisposable; + this.disposable2x2 = Disposable.None; if (this.root.children.length !== 2) { return; diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 1d560a8afec..bbfe7f0b134 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -282,6 +282,11 @@ export class ListView implements ISpliceable, IDisposable { private insertItemInDOM(index: number, beforeElement: HTMLElement | null): void { const item = this.items[index]; + if (!item) { + console.log(this.items); + throw new Error(`Got index ${index} and there are ${this.items.length} items. File issue to joao!`); + } + if (!item.row) { item.row = this.cache.alloc(item.templateId); } @@ -311,6 +316,12 @@ export class ListView implements ISpliceable, IDisposable { private removeItemFromDOM(index: number): void { const item = this.items[index]; + + if (!item) { + console.log(this.items); + throw new Error(`Got index ${index} and there are ${this.items.length} items. File issue to joao!`); + } + this.cache.release(item.row); item.row = null; } @@ -371,7 +382,12 @@ export class ListView implements ISpliceable, IDisposable { } private onScroll(e: ScrollEvent): void { - this.render(e.scrollTop, e.height); + try { + this.render(e.scrollTop, e.height); + } catch (err) { + console.log('Got bad scroll event:', e); + throw err; + } } private onTouchChange(event: GestureEvent): void { diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 5234c457c62..2eb053fbdc0 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -13,7 +13,7 @@ import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { Event } from 'vs/base/common/event'; import { addClass, EventType, EventHelper, EventLike } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { $ } from 'vs/base/browser/builder'; +import { $, Builder } from 'vs/base/browser/builder'; export interface IMenuOptions { context?: any; @@ -152,6 +152,7 @@ class MenuActionItem extends ActionItem { class SubmenuActionItem extends MenuActionItem { private mysubmenu: Menu; + private submenuContainer: Builder; private mouseOver: boolean; constructor( @@ -224,18 +225,23 @@ class SubmenuActionItem extends MenuActionItem { if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) { this.parentData.submenu.dispose(); this.parentData.submenu = null; + + if (this.submenuContainer) { + this.submenuContainer.dispose(); + this.submenuContainer = null; + } } } private createSubmenu() { if (!this.parentData.submenu) { - const submenuContainer = $(this.builder).div({ class: 'monaco-submenu menubar-menu-items-holder context-view' }); + this.submenuContainer = $(this.builder).div({ class: 'monaco-submenu menubar-menu-items-holder context-view' }); - $(submenuContainer).style({ + $(this.submenuContainer).style({ 'left': `${$(this.builder).getClientArea().width}px` }); - $(submenuContainer).on(EventType.KEY_UP, (e) => { + $(this.submenuContainer).on(EventType.KEY_UP, (e) => { let event = new StandardKeyboardEvent(e as KeyboardEvent); if (event.equals(KeyCode.LeftArrow)) { EventHelper.stop(e, true); @@ -243,10 +249,13 @@ class SubmenuActionItem extends MenuActionItem { this.parentData.parent.focus(); this.parentData.submenu.dispose(); this.parentData.submenu = null; + + this.submenuContainer.dispose(); + this.submenuContainer = null; } }); - $(submenuContainer).on(EventType.KEY_DOWN, (e) => { + $(this.submenuContainer).on(EventType.KEY_DOWN, (e) => { let event = new StandardKeyboardEvent(e as KeyboardEvent); if (event.equals(KeyCode.LeftArrow)) { EventHelper.stop(e, true); @@ -254,10 +263,24 @@ class SubmenuActionItem extends MenuActionItem { }); - this.parentData.submenu = new Menu(submenuContainer.getHTMLElement(), this.submenuActions, this.submenuOptions); + this.parentData.submenu = new Menu(this.submenuContainer.getHTMLElement(), this.submenuActions, this.submenuOptions); this.parentData.submenu.focus(); this.mysubmenu = this.parentData.submenu; } } + + public dispose() { + super.dispose(); + + if (this.mysubmenu) { + this.mysubmenu.dispose(); + this.mysubmenu = null; + } + + if (this.submenuContainer) { + this.submenuContainer.dispose(); + this.submenuContainer = null; + } + } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts index e55a3c61e92..0b1154a98fd 100644 --- a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts +++ b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts @@ -2,14 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import 'vs/css!./octicons/octicons'; import 'vs/css!./octicons/octicons-animations'; import { escape } from 'vs/base/common/strings'; function expand(text: string): string { - return text.replace(/\$\(((.+?)(~(.*?))?)\)/g, (match, g1, name, g3, animation) => { + return text.replace(/\$\(((.+?)(~(.*?))?)\)/g, (_match, _g1, name, _g3, animation) => { return ``; }); } @@ -20,11 +19,9 @@ export function renderOcticons(label: string): string { export class OcticonLabel { - private readonly _container: HTMLElement; - - constructor(container: HTMLElement) { - this._container = container; - } + constructor( + private readonly _container: HTMLElement + ) { } set text(text: string) { this._container.innerHTML = renderOcticons(text || ''); diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 9d98c12dfb7..6cd72d3311f 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./tree'; -import { IDisposable, dispose, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; import { TreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/treeModel'; import { IIterator, empty } from 'vs/base/common/iterator'; @@ -83,7 +83,7 @@ class TreeRenderer implements IRenderer, ITreeLis const contents = append(el, $('.tl-contents')); const templateData = this.renderer.renderTemplate(contents); - return { twistie, elementDisposable: EmptyDisposable, templateData }; + return { twistie, elementDisposable: Disposable.None, templateData }; } renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { diff --git a/src/vs/base/browser/ui/tree/treeModel.ts b/src/vs/base/browser/ui/tree/treeModel.ts index 7acf8a90a4b..8d3c14b727b 100644 --- a/src/vs/base/browser/ui/tree/treeModel.ts +++ b/src/vs/base/browser/ui/tree/treeModel.ts @@ -7,7 +7,6 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { IIterator, map, collect, iter, empty } from 'vs/base/common/iterator'; -import { last } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; export interface ITreeElement { @@ -108,7 +107,8 @@ export class TreeModel { const treeListElementsToInsert: ITreeNode[] = []; const elementsToInsert = getTreeElementIterator(toInsert); const nodesToInsert = collect(map(elementsToInsert, el => treeElementToNode(el, parentNode, visible, treeListElementsToInsert))); - const deletedNodes = parentNode.children.splice(last(location), deleteCount, ...nodesToInsert); + const lastIndex = location[location.length - 1]; + const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert); const visibleDeleteCount = getVisibleCount(deletedNodes); parentNode.visibleCount += getVisibleCount(nodesToInsert) - visibleDeleteCount; @@ -175,7 +175,7 @@ export class TreeModel { private findNode(location: number[]): { node: IMutableTreeNode, listIndex: number, visible: boolean } { const { parentNode, listIndex, visible } = this.findParentNode(location); - const index = last(location); + const index = location[location.length - 1]; if (index < 0 || index > parentNode.children.length) { throw new Error('Invalid tree location'); diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 98fdb2262ae..0a8cd0b2eb0 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -24,10 +24,6 @@ export function tail2(arr: T[]): [T[], T] { return [arr.slice(0, arr.length - 1), arr[arr.length - 1]]; } -export function last(arr: T[]): T { - return arr[arr.length - 1]; -} - export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { if (one.length !== other.length) { return false; @@ -81,48 +77,56 @@ export function findFirstInSorted(array: T[], p: (x: T) => boolean): number { return low; } +type Compare = (a: T, b: T) => number; + /** * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` * so only use this when actually needing stable sort. */ -export function mergeSort(data: T[], compare: (a: T, b: T) => number): T[] { - _divideAndMerge(data, compare); +export function mergeSort(data: T[], compare: Compare): T[] { + _sort(data, compare, 0, data.length - 1, []); return data; } -function _divideAndMerge(data: T[], compare: (a: T, b: T) => number): void { - if (data.length <= 1) { - // sorted - return; +function _merge(a: T[], compare: Compare, lo: number, mid: number, hi: number, aux: T[]): void { + let leftIdx = lo, rightIdx = mid + 1; + for (let i = lo; i <= hi; i++) { + aux[i] = a[i]; } - const p = (data.length / 2) | 0; - const left = data.slice(0, p); - const right = data.slice(p); - - _divideAndMerge(left, compare); - _divideAndMerge(right, compare); - - let leftIdx = 0; - let rightIdx = 0; - let i = 0; - while (leftIdx < left.length && rightIdx < right.length) { - let ret = compare(left[leftIdx], right[rightIdx]); - if (ret <= 0) { - // smaller_equal -> take left to preserve order - data[i++] = left[leftIdx++]; + for (let i = lo; i <= hi; i++) { + if (leftIdx > mid) { + // left side consumed + a[i] = aux[rightIdx++]; + } else if (rightIdx > hi) { + // right side consumed + a[i] = aux[leftIdx++]; + } else if (compare(aux[rightIdx], aux[leftIdx]) < 0) { + // right element is less -> comes first + a[i] = aux[rightIdx++]; } else { - // greater -> take right - data[i++] = right[rightIdx++]; + // left element comes first (less or equal) + a[i] = aux[leftIdx++]; } } - while (leftIdx < left.length) { - data[i++] = left[leftIdx++]; - } - while (rightIdx < right.length) { - data[i++] = right[rightIdx++]; - } } +function _sort(a: T[], compare: Compare, lo: number, hi: number, aux: T[]) { + if (hi <= lo) { + return; + } + let mid = lo + ((hi - lo) / 2) | 0; + _sort(a, compare, lo, mid, aux); + _sort(a, compare, mid + 1, hi, aux); + if (compare(a[mid], a[mid + 1]) <= 0) { + // left and right are sorted and if the last-left element is less + // or equals than the first-right element there is nothing else + // to do + return; + } + _merge(a, compare, lo, mid, hi, aux); +} + + export function groupBy(data: T[], compare: (a: T, b: T) => number): T[][] { const result: T[][] = []; let currentGroup: T[]; diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 53a7e38ee3c..4c06af4e2f5 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -32,6 +32,40 @@ export function toWinJsPromise(arg: Thenable | TPromise): TPromise { return new TPromise((resolve, reject) => arg.then(resolve, reject)); } +export interface CancelablePromise extends Promise { + cancel(): void; +} + +export function createCancelablePromise(callback: (token: CancellationToken) => Thenable): CancelablePromise { + const source = new CancellationTokenSource(); + + const thenable = callback(source.token); + const promise = new Promise((resolve, reject) => { + source.token.onCancellationRequested(() => { + reject(errors.canceled()); + }); + Promise.resolve(thenable).then(value => { + source.dispose(); + resolve(value); + }, err => { + source.dispose(); + reject(err); + }); + }); + + return new class implements CancelablePromise { + cancel() { + source.cancel(); + } + then(resolve?: ((value: T) => TResult1 | Thenable) | undefined | null, reject?: ((reason: any) => TResult2 | Thenable) | undefined | null): Promise { + return promise.then(resolve, reject); + } + catch(reject?: ((reason: any) => TResult | Thenable) | undefined | null): Promise { + return this.then(undefined, reject); + } + }; +} + export function asWinJsPromise(callback: (token: CancellationToken) => T | TPromise | Thenable): TPromise { let source = new CancellationTokenSource(); return new TPromise((resolve, reject, progress) => { @@ -318,7 +352,7 @@ export class Barrier { constructor() { this._isOpen = false; - this._promise = new TPromise((c, e, p) => { + this._promise = new TPromise((c, e) => { this._completePromise = c; }, () => { console.warn('You should really not try to cancel this ready promise!'); @@ -364,8 +398,16 @@ export class ShallowCancelThenPromise extends TPromise { /** * Replacement for `WinJS.TPromise.timeout`. */ -export function timeout(n: number): Thenable { - return new TPromise(resolve => setTimeout(resolve, n)); +export function timeout(n: number): CancelablePromise { + return createCancelablePromise(token => { + return new Promise((resolve, reject) => { + const handle = setTimeout(resolve, n); + token.onCancellationRequested(_ => { + clearTimeout(handle); + reject(errors.canceled()); + }); + }); + }); } function isWinJSPromise(candidate: any): candidate is TPromise { @@ -442,13 +484,35 @@ export function sequence(promiseFactories: ITask>[]): TPromise(promiseFactories: ITask>[], shouldStop: (t: T) => boolean = t => !!t): TPromise { +export function first2(promiseFactories: ITask>[], shouldStop: (t: T) => boolean = t => !!t, defaultValue: T = null): Promise { + + let index = 0; + const len = promiseFactories.length; + + const loop = () => { + if (index >= len) { + return Promise.resolve(defaultValue); + } + const factory = promiseFactories[index++]; + const promise = factory(); + return promise.then(result => { + if (shouldStop(result)) { + return Promise.resolve(result); + } + return loop(); + }); + }; + + return loop(); +} + +export function first(promiseFactories: ITask>[], shouldStop: (t: T) => boolean = t => !!t, defaultValue: T = null): TPromise { let index = 0; const len = promiseFactories.length; const loop: () => TPromise = () => { if (index >= len) { - return TPromise.as(null); + return TPromise.as(defaultValue); } const factory = promiseFactories[index++]; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 99b3fed4001..eb4c7cef1c1 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -6,7 +6,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { once as onceFn } from 'vs/base/common/functional'; -import { combinedDisposable, empty as EmptyDisposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -423,6 +423,8 @@ export function forEach(event: Event, each: (i: I) => void): Event { return (listener, thisArgs = null, disposables?) => event(i => { each(i); listener.call(thisArgs, i); }, null, disposables); } +export function filterEvent(event: Event, filter: (e: T) => boolean): Event; +export function filterEvent(event: Event, filter: (e: T | R) => e is R): Event; export function filterEvent(event: Event, filter: (e: T) => boolean): Event { return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables); } @@ -559,7 +561,7 @@ export class Relay implements IDisposable { private emitter = new Emitter(); readonly event: Event = this.emitter.event; - private disposable: IDisposable = EmptyDisposable; + private disposable: IDisposable = Disposable.None; set input(event: Event) { this.disposable.dispose(); diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index befc15c2676..f2a5cf2da1d 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -7,10 +7,6 @@ import { once } from 'vs/base/common/functional'; -export const empty: IDisposable = Object.freeze({ - dispose() { } -}); - export interface IDisposable { dispose(): void; } @@ -24,7 +20,6 @@ export function dispose(disposable: T): T; export function dispose(...disposables: T[]): T[]; export function dispose(disposables: T[]): T[]; export function dispose(first: T | T[], ...rest: T[]): T | T[] { - if (Array.isArray(first)) { first.forEach(d => d && d.dispose()); return []; @@ -45,18 +40,14 @@ export function combinedDisposable(disposables: IDisposable[]): IDisposable { return { dispose: () => dispose(disposables) }; } -export function toDisposable(...fns: (() => void)[]): IDisposable { - return { - dispose() { - for (const fn of fns) { - fn(); - } - } - }; +export function toDisposable(fn: () => void): IDisposable { + return { dispose() { fn(); } }; } export abstract class Disposable implements IDisposable { + static None = Object.freeze({ dispose() { } }); + protected _toDispose: IDisposable[] = []; protected get toDispose(): IDisposable[] { return this._toDispose; } diff --git a/src/vs/base/common/winjs.base.d.ts b/src/vs/base/common/winjs.base.d.ts index b176120a06b..f5b941a8873 100644 --- a/src/vs/base/common/winjs.base.d.ts +++ b/src/vs/base/common/winjs.base.d.ts @@ -39,7 +39,6 @@ export declare class Promise { public static join(promises: [T1 | PromiseLike, T2 | PromiseLike]): Promise<[T1, T2]>; public static join(promises: (T | PromiseLike)[]): Promise; - public static join(promises: { [n: string]: T | PromiseLike }): Promise<{ [n: string]: T }>; public static any(promises: (T | PromiseLike)[]): Promise<{ key: string; value: Promise; }>; diff --git a/src/vs/base/common/worker/simpleWorker.ts b/src/vs/base/common/worker/simpleWorker.ts index ceaaa0ff541..8b0862a816d 100644 --- a/src/vs/base/common/worker/simpleWorker.ts +++ b/src/vs/base/common/worker/simpleWorker.ts @@ -91,7 +91,7 @@ class SimpleWorkerProtocol { c: null, e: null }; - let result = new TPromise((c, e, p) => { + let result = new TPromise((c, e) => { reply.c = c; reply.e = e; }, () => { @@ -232,7 +232,7 @@ export class SimpleWorkerClient extends Disposable { loaderConfiguration = (self).requirejs.s.contexts._.config; } - this._lazyProxy = new TPromise((c, e, p) => { + this._lazyProxy = new TPromise((c, e) => { lazyProxyFulfill = c; lazyProxyReject = e; }, () => { /* no cancel */ }); @@ -273,7 +273,7 @@ export class SimpleWorkerClient extends Disposable { } private _request(method: string, args: any[]): TPromise { - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { this._onModuleLoaded.then(() => { this._protocol.sendMessage(method, args).then(c, e); }, e); @@ -363,7 +363,7 @@ export class SimpleWorkerServer { let cc: ValueCallback; let ee: ErrorCallback; - let r = new TPromise((c, e, p) => { + let r = new TPromise((c, e) => { cc = c; ee = e; }); diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index ea32b4dce2f..eb01c197f74 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -234,7 +234,7 @@ export abstract class AbstractProcess { if (this.cmd) { childProcess = cp.spawn(this.cmd, this.args, this.options); } else if (this.module) { - this.childProcessPromise = new TPromise((c, e, p) => { + this.childProcessPromise = new TPromise((c, e) => { fork(this.module, this.args, this.options, (error: any, childProcess: cp.ChildProcess) => { if (error) { e(error); @@ -323,7 +323,7 @@ export abstract class AbstractProcess { } private useExec(): TPromise { - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { if (!this.shell || !Platform.isWindows) { c(false); } diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index f0410cc42d4..a7d2d36d0ca 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -206,7 +206,8 @@ export function listProcesses(rootPid: number): Promise { // The cpu usage value reported on Linux is the average over the process lifetime, // recalculate the usage over a one second interval - let cmd = URI.parse(require.toUrl('vs/base/node/cpuUsage.sh')).fsPath; + // JSON.stringify is needed to escape spaces, https://github.com/nodejs/node/issues/6803 + let cmd = JSON.stringify(URI.parse(require.toUrl('vs/base/node/cpuUsage.sh')).fsPath); cmd += ' ' + pids.join(' '); exec(cmd, {}, (err, stdout, stderr) => { diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index a3985361b63..ed35efadee0 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -6,17 +6,21 @@ 'use strict'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter, once, filterEvent } from 'vs/base/common/event'; +import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Event, Emitter, once, filterEvent, toPromise, Relay } from 'vs/base/common/event'; enum MessageType { - RequestCommon, - RequestCancel, + RequestPromise, + RequestPromiseCancel, ResponseInitialize, - ResponseSuccess, - ResponseProgress, - ResponseError, - ResponseErrorObj + ResponsePromiseSuccess, + ResponsePromiseProgress, + ResponsePromiseError, + ResponsePromiseErrorObj, + + RequestEventListen, + RequestEventDispose, + ResponseEventFire, } function isResponse(messageType: MessageType): boolean { @@ -36,7 +40,6 @@ interface IRawRequest extends IRawMessage { interface IRequest { raw: IRawRequest; - emitter?: Emitter; flush?: () => void; } @@ -65,7 +68,8 @@ enum State { * with at most one single return value. */ export interface IChannel { - call(command: string, arg?: any): TPromise; + call(command: string, arg?: any): TPromise; + listen(event: string, arg?: any): Event; } /** @@ -90,7 +94,8 @@ export interface IChannelClient { * channels (each from a separate client) to pick from. */ export interface IClientRouter { - route(command: string, arg: any): string; + routeCall(command: string, arg: any): string; + routeEvent(event: string, arg: any): string; } /** @@ -104,6 +109,8 @@ export interface IRoutingChannelClient { getChannel(channelName: string, router: IClientRouter): T; } +// TODO@joao cleanup this mess! + export class ChannelServer implements IChannelServer, IDisposable { private channels: { [name: string]: IChannel } = Object.create(null); @@ -121,17 +128,22 @@ export class ChannelServer implements IChannelServer, IDisposable { private onMessage(request: IRawRequest): void { switch (request.type) { - case MessageType.RequestCommon: - this.onCommonRequest(request); + case MessageType.RequestPromise: + this.onPromise(request); break; - case MessageType.RequestCancel: - this.onCancelRequest(request); + case MessageType.RequestEventListen: + this.onEventListen(request); + break; + + case MessageType.RequestPromiseCancel: + case MessageType.RequestEventDispose: + this.disposeActiveRequest(request); break; } } - private onCommonRequest(request: IRawRequest): void { + private onPromise(request: IRawRequest): void { const channel = this.channels[request.channelName]; let promise: Promise; @@ -144,7 +156,7 @@ export class ChannelServer implements IChannelServer, IDisposable { const id = request.id; const requestPromise = promise.then(data => { - this.protocol.send({ id, data, type: MessageType.ResponseSuccess }); + this.protocol.send({ id, data, type: MessageType.ResponsePromiseSuccess }); delete this.activeRequests[request.id]; }, data => { if (data instanceof Error) { @@ -153,21 +165,31 @@ export class ChannelServer implements IChannelServer, IDisposable { message: data.message, name: data.name, stack: data.stack ? (data.stack.split ? data.stack.split('\n') : data.stack) : void 0 - }, type: MessageType.ResponseError + }, type: MessageType.ResponsePromiseError }); } else { - this.protocol.send({ id, data, type: MessageType.ResponseErrorObj }); + this.protocol.send({ id, data, type: MessageType.ResponsePromiseErrorObj }); } delete this.activeRequests[request.id]; }, data => { - this.protocol.send({ id, data, type: MessageType.ResponseProgress }); + this.protocol.send({ id, data, type: MessageType.ResponsePromiseProgress }); }); this.activeRequests[request.id] = toDisposable(() => requestPromise.cancel()); } - private onCancelRequest(request: IRawRequest): void { + private onEventListen(request: IRawRequest): void { + const channel = this.channels[request.channelName]; + + const id = request.id; + const event = channel.listen(request.name, request.arg); + const disposable = event(data => this.protocol.send({ id, data, type: MessageType.ResponseEventFire })); + + this.activeRequests[request.id] = disposable; + } + + private disposeActiveRequest(request: IRawRequest): void { const disposable = this.activeRequests[request.id]; if (disposable) { @@ -190,63 +212,85 @@ export class ChannelServer implements IChannelServer, IDisposable { export class ChannelClient implements IChannelClient, IDisposable { - private state: State; - private activeRequests: Promise[]; - private bufferedRequests: IRequest[]; - private handlers: { [id: number]: IHandler; }; - private lastRequestId: number; + private state: State = State.Uninitialized; + private activeRequests: IDisposable[] = []; + private bufferedRequests: IRequest[] = []; + private handlers: { [id: number]: IHandler; } = Object.create(null); + private lastRequestId: number = 0; private protocolListener: IDisposable; + private _onDidInitialize = new Emitter(); + readonly onDidInitialize = this._onDidInitialize.event; + constructor(private protocol: IMessagePassingProtocol) { - this.state = State.Uninitialized; - this.activeRequests = []; - this.bufferedRequests = []; - this.handlers = Object.create(null); - this.lastRequestId = 0; this.protocolListener = this.protocol.onMessage(r => this.onMessage(r)); } getChannel(channelName: string): T { - const call = (command: string, arg: any) => this.request(channelName, command, arg); - return { call } as T; + const call = (command: string, arg: any) => this.requestPromise(channelName, command, arg); + const listen = (event: string, arg: any) => this.requestEvent(channelName, event, arg); + + return { call, listen } as T; } - private request(channelName: string, name: string, arg: any): Promise { - const request = { - raw: { - id: this.lastRequestId++, - type: MessageType.RequestCommon, - channelName, - name, - arg - } - }; + private requestPromise(channelName: string, name: string, arg: any): TPromise { + const id = this.lastRequestId++; + const type = MessageType.RequestPromise; + const request = { raw: { id, type, channelName, name, arg } }; const activeRequest = this.state === State.Uninitialized ? this.bufferRequest(request) : this.doRequest(request); - this.activeRequests.push(activeRequest); + const disposable = toDisposable(() => activeRequest.cancel()); + this.activeRequests.push(disposable); activeRequest .then(null, _ => null) - .done(() => this.activeRequests = this.activeRequests.filter(i => i !== activeRequest)); + .done(() => this.activeRequests = this.activeRequests.filter(el => el !== disposable)); return activeRequest; } + private requestEvent(channelName: string, name: string, arg: any): Event { + const id = this.lastRequestId++; + const type = MessageType.RequestEventListen; + const request = { raw: { id, type, channelName, name, arg } }; + + let uninitializedPromise: TPromise | null = null; + const emitter = new Emitter({ + onFirstListenerAdd: () => { + uninitializedPromise = this.whenInitialized().then(() => { + uninitializedPromise = null; + this.send(request.raw); + }); + }, + onLastListenerRemove: () => { + if (uninitializedPromise) { + uninitializedPromise.cancel(); + uninitializedPromise = null; + } else { + this.send({ id, type: MessageType.RequestEventDispose }); + } + } + }); + + this.handlers[id] = response => emitter.fire(response.data); + return emitter.event; + } + private doRequest(request: IRequest): Promise { const id = request.raw.id; return new TPromise((c, e, p) => { this.handlers[id] = response => { switch (response.type) { - case MessageType.ResponseSuccess: + case MessageType.ResponsePromiseSuccess: delete this.handlers[id]; c(response.data); break; - case MessageType.ResponseError: + case MessageType.ResponsePromiseError: delete this.handlers[id]; const error = new Error(response.data.message); (error).stack = response.data.stack; @@ -254,12 +298,12 @@ export class ChannelClient implements IChannelClient, IDisposable { e(error); break; - case MessageType.ResponseErrorObj: + case MessageType.ResponsePromiseErrorObj: delete this.handlers[id]; e(response.data); break; - case MessageType.ResponseProgress: + case MessageType.ResponsePromiseProgress: p(response.data); break; } @@ -267,7 +311,7 @@ export class ChannelClient implements IChannelClient, IDisposable { this.send(request.raw); }, - () => this.send({ id, type: MessageType.RequestCancel })); + () => this.send({ id, type: MessageType.RequestPromiseCancel })); } private bufferRequest(request: IRequest): Promise { @@ -309,6 +353,7 @@ export class ChannelClient implements IChannelClient, IDisposable { if (this.state === State.Uninitialized && response.type === MessageType.ResponseInitialize) { this.state = State.Idle; + this._onDidInitialize.fire(); this.bufferedRequests.forEach(r => r.flush && r.flush()); this.bufferedRequests = null; return; @@ -328,12 +373,19 @@ export class ChannelClient implements IChannelClient, IDisposable { } } + private whenInitialized(): TPromise { + if (this.state === State.Idle) { + return TPromise.as(null); + } else { + return toPromise(this.onDidInitialize); + } + } + dispose(): void { this.protocolListener.dispose(); this.protocolListener = null; - this.activeRequests.forEach(r => r.cancel()); - this.activeRequests = []; + this.activeRequests = dispose(this.activeRequests); } } @@ -381,16 +433,28 @@ export class IPCServer implements IChannelServer, IRoutingChannelClient, IDispos getChannel(channelName: string, router: IClientRouter): T { const call = (command: string, arg: any) => { - const id = router.route(command, arg); + const id = router.routeCall(command, arg); if (!id) { return TPromise.wrapError(new Error('Client id should be provided')); } - return this.getClient(id).then(client => client.getChannel(channelName).call(command, arg)); + return getDelayedChannel(this.getClient(id).then(client => client.getChannel(channelName))) + .call(command, arg); }; - return { call } as T; + const listen = (event: string, arg: any) => { + const id = router.routeEvent(event, arg); + + if (!id) { + return TPromise.wrapError(new Error('Client id should be provided')); + } + + return getDelayedChannel(this.getClient(id).then(client => client.getChannel(channelName))) + .listen(event, arg); + }; + + return { call, listen } as T; } registerChannel(channelName: string, channel: IChannel): void { @@ -453,7 +517,13 @@ export class IPCClient implements IChannelClient, IChannelServer, IDisposable { export function getDelayedChannel(promise: TPromise): T { const call = (command: string, arg: any) => promise.then(c => c.call(command, arg)); - return { call } as T; + const listen = (event: string, arg: any) => { + const relay = new Relay(); + promise.then(c => relay.input = c.listen(event, arg)); + return relay.event; + }; + + return { call, listen } as T; } export function getNextTickChannel(channel: T): T { @@ -469,34 +539,19 @@ export function getNextTickChannel(channel: T): T { .then(() => channel.call(command, arg)); }; - return { call } as T; -} - -export type Serializer = (obj: T) => R; -export type Deserializer = (raw: R) => T; - -export function eventToCall(event: Event, serializer: Serializer = t => t): TPromise { - let disposable: IDisposable; - - return new TPromise( - (c, e, p) => disposable = event(t => p(serializer(t))), - () => disposable.dispose() - ); -} - -export function eventFromCall(channel: IChannel, name: string, arg: any = null, deserializer: Deserializer = t => t): Event { - let promise: Promise; - - const emitter = new Emitter({ - onFirstListenerAdd: () => { - promise = channel.call(name, arg) - .then(null, err => null, e => emitter.fire(deserializer(e))); - }, - onLastListenerRemove: () => { - promise.cancel(); - promise = null; + const listen = (event: string, arg: any): Event => { + if (didTick) { + return channel.listen(event, arg); } - }); - return emitter.event; + const relay = new Relay(); + + TPromise.timeout(0) + .then(() => didTick = true) + .then(() => relay.input = channel.listen(event, arg)); + + return relay.event; + }; + + return { call, listen } as T; } diff --git a/src/vs/base/parts/ipc/node/ipc.cp.ts b/src/vs/base/parts/ipc/node/ipc.cp.ts index 40d8fb8ff6b..38a8a96f813 100644 --- a/src/vs/base/parts/ipc/node/ipc.cp.ts +++ b/src/vs/base/parts/ipc/node/ipc.cp.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { ChildProcess, fork, ForkOptions } from 'child_process'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { Delayer } from 'vs/base/common/async'; import { deepClone, assign } from 'vs/base/common/objects'; -import { Emitter, fromNodeEventEmitter } from 'vs/base/common/event'; +import { Emitter, fromNodeEventEmitter, Event } from 'vs/base/common/event'; import { createQueuedSender } from 'vs/base/node/processes'; import { ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { isRemoteConsoleLog, log } from 'vs/base/node/console'; @@ -74,11 +74,14 @@ export interface IIPCOptions { export class Client implements IChannelClient, IDisposable { private disposeDelayer: Delayer; - private activeRequests: TPromise[]; + private activeRequests: IDisposable[]; private child: ChildProcess; private _client: IPCClient; private channels: { [name: string]: IChannel }; + private _onDidProcessExit = new Emitter<{ code: number, signal: string }>(); + readonly onDidProcessExit = this._onDidProcessExit.event; + constructor(private modulePath: string, private options: IIPCOptions) { const timeout = options && options.timeout ? options.timeout : 60000; this.disposeDelayer = new Delayer(timeout); @@ -89,11 +92,12 @@ export class Client implements IChannelClient, IDisposable { } getChannel(channelName: string): T { - const call = (command: string, arg: any) => this.request(channelName, command, arg); - return { call } as T; + const call = (command: string, arg: any) => this.requestPromise(channelName, command, arg); + const listen = (event: string, arg: any) => this.requestEvent(channelName, event, arg); + return { call, listen } as IChannel as T; } - protected request(channelName: string, name: string, arg: any): TPromise { + protected requestPromise(channelName: string, name: string, arg: any): TPromise { if (!this.disposeDelayer) { return TPromise.wrapError(new Error('disposed')); } @@ -110,7 +114,7 @@ export class Client implements IChannelClient, IDisposable { return; } - this.activeRequests.splice(this.activeRequests.indexOf(result), 1); + this.activeRequests.splice(this.activeRequests.indexOf(disposable), 1); if (this.activeRequests.length === 0) { this.disposeDelayer.trigger(() => this.disposeClient()); @@ -118,10 +122,44 @@ export class Client implements IChannelClient, IDisposable { }); }, () => request.cancel()); - this.activeRequests.push(result); + const disposable = toDisposable(() => result.cancel()); + this.activeRequests.push(disposable); return result; } + protected requestEvent(channelName: string, name: string, arg: any): Event { + if (!this.disposeDelayer) { + return Event.None; + } + + this.disposeDelayer.cancel(); + + let listener: IDisposable; + const emitter = new Emitter({ + onFirstListenerAdd: () => { + const channel = this.channels[channelName] || (this.channels[channelName] = this.client.getChannel(channelName)); + const event: Event = channel.listen(name, arg); + + listener = event(emitter.fire, emitter); + this.activeRequests.push(listener); + + }, + onLastListenerRemove: () => { + if (!this.activeRequests) { + return; + } + + this.activeRequests.splice(this.activeRequests.indexOf(listener), 1); + + if (this.activeRequests.length === 0) { + this.disposeDelayer.trigger(() => this.disposeClient()); + } + } + }); + + return emitter.event; + } + private get client(): IPCClient { if (!this._client) { const args = this.options && this.options.args ? this.options.args : []; @@ -178,8 +216,7 @@ export class Client implements IChannelClient, IDisposable { process.removeListener('exit', onExit); if (this.activeRequests) { - this.activeRequests.forEach(req => req.cancel()); - this.activeRequests = []; + this.activeRequests = dispose(this.activeRequests); } if (code !== 0 && signal !== 'SIGTERM') { @@ -187,6 +224,8 @@ export class Client implements IChannelClient, IDisposable { this.disposeDelayer.cancel(); this.disposeClient(); } + + this._onDidProcessExit.fire({ code, signal }); }); } @@ -203,6 +242,7 @@ export class Client implements IChannelClient, IDisposable { } dispose() { + this._onDidProcessExit.dispose(); this.disposeDelayer.cancel(); this.disposeDelayer = null; this.disposeClient(); diff --git a/src/vs/base/parts/ipc/test/node/ipc.test.ts b/src/vs/base/parts/ipc/test/node/ipc.test.ts index bd9c57671ae..3091fce1d7f 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.test.ts @@ -24,10 +24,6 @@ suite('IPC', () => { suite('child process', () => { test('createChannel', () => { - if (process.env['VSCODE_PID']) { - return undefined; // this test fails when run from within VS Code - } - const client = createClient(); const channel = client.getChannel('test'); const service = new TestServiceClient(channel); @@ -41,10 +37,6 @@ suite('IPC', () => { }); test('cancellation', () => { - if (process.env['VSCODE_PID']) { - return undefined; // this test fails when run from within VS Code - } - const client = createClient(); const channel = client.getChannel('test'); const service = new TestServiceClient(channel); @@ -61,10 +53,6 @@ suite('IPC', () => { }); test('events', () => { - if (process.env['VSCODE_PID']) { - return undefined; // this test fails when run from within VS Code - } - const client = createClient(); const channel = client.getChannel('test'); const service = new TestServiceClient(channel); @@ -87,10 +75,6 @@ suite('IPC', () => { }); test('event dispose', () => { - if (process.env['VSCODE_PID']) { - return undefined; // this test fails when run from within VS Code - } - const client = createClient(); const channel = client.getChannel('test'); const service = new TestServiceClient(channel); diff --git a/src/vs/base/parts/ipc/test/node/testService.ts b/src/vs/base/parts/ipc/test/node/testService.ts index 453ea9c85e0..a53ff5ed37b 100644 --- a/src/vs/base/parts/ipc/test/node/testService.ts +++ b/src/vs/base/parts/ipc/test/node/testService.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise, PPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event, Emitter } from 'vs/base/common/event'; export interface IMarcoPoloEvent { @@ -68,6 +68,9 @@ export class TestService implements ITestService { } export interface ITestChannel extends IChannel { + listen(event: 'marco'): Event; + listen(event: string, arg?: any): Event; + call(command: 'marco'): TPromise; call(command: 'pong', ping: string): TPromise; call(command: 'cancelMe'): TPromise; @@ -79,12 +82,19 @@ export class TestChannel implements ITestChannel { constructor(private testService: ITestService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'marco': return this.testService.onMarco; + } + + throw new Error('Event not found'); + } + call(command: string, ...args: any[]): TPromise { switch (command) { case 'pong': return this.testService.pong(args[0]); case 'cancelMe': return this.testService.cancelMe(); case 'marco': return this.testService.marco(); - case 'event:marco': return eventToCall(this.testService.onMarco); case 'batchPerf': return this.testService.batchPerf(args[0].batches, args[0].size, args[0].dataSize); default: return TPromise.wrapError(new Error('command not found')); } @@ -93,12 +103,9 @@ export class TestChannel implements ITestChannel { export class TestServiceClient implements ITestService { - private _onMarco: Event; - get onMarco(): Event { return this._onMarco; } + get onMarco(): Event { return this.channel.listen('marco'); } - constructor(private channel: ITestChannel) { - this._onMarco = eventFromCall(channel, 'event:marco'); - } + constructor(private channel: ITestChannel) { } marco(): TPromise { return this.channel.call('marco'); diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index b5c4ca60e62..032e70e54b7 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -175,9 +175,9 @@ export class DefaultController implements _.IController { tree.clearFocus(payload); tree.clearSelection(payload); } else { - const isMouseDown = eventish && event.browserEvent && event.browserEvent.type === 'mousedown'; - if (!isMouseDown) { - eventish.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise + const isSingleMouseDown = eventish && event.browserEvent && event.browserEvent.type === 'mousedown' && event.browserEvent.detail === 1; + if (!isSingleMouseDown) { + eventish.preventDefault(); // we cannot preventDefault onMouseDown with single click because this would break DND otherwise } eventish.stopPropagation(); diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index ee1a6e18b58..cff3a614965 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -52,6 +52,11 @@ suite('Arrays', () => { assert.deepEqual(data, [1, 2, 3, 4, 5, 6, 7, 8]); }); + test('mergeSort, sorted array', function () { + let data = arrays.mergeSort([1, 2, 3, 4, 5, 6], (a, b) => a - b); + assert.deepEqual(data, [1, 2, 3, 4, 5, 6]); + }); + test('mergeSort, is stable', function () { let numbers = arrays.mergeSort([33, 22, 11, 4, 99, 1], (a, b) => 0); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index d1ef408cb3f..9cab77a9881 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -6,16 +6,55 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as Async from 'vs/base/common/async'; +import * as async from 'vs/base/common/async'; import URI from 'vs/base/common/uri'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; suite('Async', () => { + test('cancelablePromise - set token, don\'t wait for inner promise', function () { + let canceled = 0; + let promise = async.createCancelablePromise(token => { + token.onCancellationRequested(_ => { canceled += 1; }); + return new Promise(resolve => { /*never*/ }); + }); + let result = promise.then(_ => assert.ok(false), err => { + assert.equal(canceled, 1); + assert.ok(isPromiseCanceledError(err)); + }); + promise.cancel(); + promise.cancel(); // cancel only once + return result; + }); + + test('cancelablePromise - cancel despite inner promise being resolved', function () { + let canceled = 0; + let promise = async.createCancelablePromise(token => { + token.onCancellationRequested(_ => { canceled += 1; }); + return Promise.resolve(1234); + }); + let result = promise.then(_ => assert.ok(false), err => { + assert.equal(canceled, 1); + assert.ok(isPromiseCanceledError(err)); + }); + promise.cancel(); + return result; + }); + + test('cancelablePromise - get inner result', async function () { + let promise = async.createCancelablePromise(token => { + return async.timeout(12).then(_ => 1234); + }); + + let result = await promise; + assert.equal(result, 1234); + }); + test('asDisposablePromise', async function () { - let value = await Async.asDisposablePromise(TPromise.as(1)).promise; + let value = await async.asDisposablePromise(TPromise.as(1)).promise; assert.equal(value, 1); - let disposablePromise = Async.asDisposablePromise(TPromise.timeout(1000).then(_ => 1), 2); + let disposablePromise = async.asDisposablePromise(TPromise.timeout(1000).then(_ => 1), 2); disposablePromise.dispose(); value = await disposablePromise.promise; assert.equal(value, 2); @@ -27,7 +66,7 @@ suite('Async', () => { return TPromise.as(++count); }; - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); return TPromise.join([ throttler.queue(factory).then((result) => { assert.equal(result, 1); }), @@ -46,7 +85,7 @@ suite('Async', () => { }); }; - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); return TPromise.join([ throttler.queue(factory).then((result) => { assert.equal(result, 1); }), @@ -73,7 +112,7 @@ suite('Async', () => { }); }; - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); let p1: TPromise; const p = TPromise.join([ @@ -96,7 +135,7 @@ suite('Async', () => { }); }; - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); let p2: TPromise; const p = TPromise.join([ @@ -119,7 +158,7 @@ suite('Async', () => { }); }; - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); let p3: TPromise; const p = TPromise.join([ @@ -139,7 +178,7 @@ suite('Async', () => { return TPromise.timeout(0).then(() => n); }; - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); let promises: TPromise[] = []; @@ -159,7 +198,7 @@ suite('Async', () => { }); }); - let throttler = new Async.Throttler(); + let throttler = new async.Throttler(); let promises: TPromise[] = []; let progresses: any[][] = [[], [], []]; @@ -180,7 +219,7 @@ suite('Async', () => { return TPromise.as(++count); }; - let delayer = new Async.Delayer(0); + let delayer = new async.Delayer(0); let promises: TPromise[] = []; assert(!delayer.isTriggered()); @@ -205,7 +244,7 @@ suite('Async', () => { return TPromise.as(++count); }; - let delayer = new Async.Delayer(0); + let delayer = new async.Delayer(0); assert(!delayer.isTriggered()); @@ -228,7 +267,7 @@ suite('Async', () => { return TPromise.as(++count); }; - let delayer = new Async.Delayer(0); + let delayer = new async.Delayer(0); let promises: TPromise[] = []; assert(!delayer.isTriggered()); @@ -255,7 +294,7 @@ suite('Async', () => { return TPromise.as(++count); }; - let delayer = new Async.Delayer(0); + let delayer = new async.Delayer(0); let promises: TPromise[] = []; assert(!delayer.isTriggered()); @@ -307,7 +346,7 @@ suite('Async', () => { return TPromise.as(n); }; - let delayer = new Async.Delayer(0); + let delayer = new async.Delayer(0); let promises: TPromise[] = []; assert(!delayer.isTriggered()); @@ -334,7 +373,7 @@ suite('Async', () => { }); }); - let delayer = new Async.Delayer(0); + let delayer = new async.Delayer(0); let promises: TPromise[] = []; let progresses: any[][] = [[], [], []]; @@ -358,7 +397,7 @@ suite('Async', () => { }); }); - let delayer = new Async.ThrottledDelayer(0); + let delayer = new async.ThrottledDelayer(0); let promises: TPromise[] = []; let progresses: any[][] = [[], [], []]; @@ -378,7 +417,7 @@ suite('Async', () => { return TPromise.as(n); }; - return Async.sequence([ + return async.sequence([ factoryFactory(1), factoryFactory(2), factoryFactory(3), @@ -399,7 +438,7 @@ suite('Async', () => { return TPromise.as(n); }; - let limiter = new Async.Limiter(1); + let limiter = new async.Limiter(1); let promises: TPromise[] = []; [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); @@ -407,7 +446,7 @@ suite('Async', () => { return TPromise.join(promises).then((res) => { assert.equal(10, res.length); - limiter = new Async.Limiter(100); + limiter = new async.Limiter(100); promises = []; [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); @@ -423,14 +462,14 @@ suite('Async', () => { return TPromise.timeout(0).then(() => n); }; - let limiter = new Async.Limiter(1); + let limiter = new async.Limiter(1); let promises: TPromise[] = []; [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); return TPromise.join(promises).then((res) => { assert.equal(10, res.length); - limiter = new Async.Limiter(100); + limiter = new async.Limiter(100); promises = []; [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); @@ -449,7 +488,7 @@ suite('Async', () => { return TPromise.timeout(0).then(() => { activePromises--; return n; }); }; - let limiter = new Async.Limiter(5); + let limiter = new async.Limiter(5); let promises: TPromise[] = []; [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); @@ -461,7 +500,7 @@ suite('Async', () => { }); test('Queue - simple', function () { - let queue = new Async.Queue(); + let queue = new async.Queue(); let syncPromise = false; let f1 = () => TPromise.as(true).then(() => syncPromise = true); @@ -484,7 +523,7 @@ suite('Async', () => { }); test('Queue - order is kept', function () { - let queue = new Async.Queue(); + let queue = new async.Queue(); let res: number[] = []; @@ -508,7 +547,7 @@ suite('Async', () => { }); test('Queue - errors bubble individually but not cause stop', function () { - let queue = new Async.Queue(); + let queue = new async.Queue(); let res: number[] = []; let error = false; @@ -533,7 +572,7 @@ suite('Async', () => { }); test('Queue - order is kept (chained)', function () { - let queue = new Async.Queue(); + let queue = new async.Queue(); let res: number[] = []; @@ -561,7 +600,7 @@ suite('Async', () => { }); test('Queue - events', function (done) { - let queue = new Async.Queue(); + let queue = new async.Queue(); let finished = false; queue.onFinished(() => { @@ -587,7 +626,7 @@ suite('Async', () => { }); test('ResourceQueue - simple', function () { - let queue = new Async.ResourceQueue(); + let queue = new async.ResourceQueue(); const r1Queue = queue.queueFor(URI.file('/some/path')); const r2Queue = queue.queueFor(URI.file('/some/other/path')); diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 2c1e18bf6ef..6a613730db3 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -5,9 +5,9 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; -import { distinctParents, dirname } from 'vs/base/common/resources'; import { normalize } from 'vs/base/common/paths'; +import { dirname, distinctParents, joinPath } from 'vs/base/common/resources'; +import URI from 'vs/base/common/uri'; suite('Resources', () => { @@ -50,4 +50,22 @@ suite('Resources', () => { // does not explode (https://github.com/Microsoft/vscode/issues/41987) dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' })); }); + + test('joinPath', () => { + assert.equal( + joinPath(URI.file('/foo/bar'), '/file.js').toString(), + 'file:///foo/bar/file.js'); + + assert.equal( + joinPath(URI.file('/foo/bar/'), '/file.js').toString(), + 'file:///foo/bar/file.js'); + + assert.equal( + joinPath(URI.file('/'), '/file.js').toString(), + 'file:///file.js'); + + assert.equal( + joinPath(URI.from({ scheme: 'myScheme', authority: 'authority', path: '/path', query: 'query', fragment: 'fragment' }), '/file.js').toString(), + 'myScheme://authority/path/file.js?query#fragment'); + }); }); \ No newline at end of file diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index 1875a6f6450..f0e948976fa 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -8,7 +8,7 @@ import * as errors from 'vs/base/common/errors'; import * as paths from 'vs/base/common/paths'; import URI from 'vs/base/common/uri'; -import { PPromise, ProgressCallback, TProgressCallback, TPromise, TValueCallback } from 'vs/base/common/winjs.base'; +import { PPromise, TProgressCallback, TPromise, TValueCallback } from 'vs/base/common/winjs.base'; export class DeferredTPromise extends TPromise { @@ -16,17 +16,15 @@ export class DeferredTPromise extends TPromise { private completeCallback: TValueCallback; private errorCallback: (err: any) => void; - private progressCallback: ProgressCallback; constructor() { let captured: any; - super((c, e, p) => { - captured = { c, e, p }; + super((c, e) => { + captured = { c, e }; }, () => this.oncancel()); this.canceled = false; this.completeCallback = captured.c; this.errorCallback = captured.e; - this.progressCallback = captured.p; } public complete(value: T) { @@ -37,10 +35,6 @@ export class DeferredTPromise extends TPromise { this.errorCallback(err); } - public progress(p: any) { - this.progressCallback(p); - } - private oncancel(): void { this.canceled = true; } diff --git a/src/vs/base/test/common/winjs.promise.test.ts b/src/vs/base/test/common/winjs.promise.test.ts index a9b3038b457..c8f0070d124 100644 --- a/src/vs/base/test/common/winjs.promise.test.ts +++ b/src/vs/base/test/common/winjs.promise.test.ts @@ -11,7 +11,7 @@ suite('WinJS and ES6 Promises', function () { test('Promise.resolve', function () { let resolveTPromise; - const tPromise = new winjs.Promise(function (c, e, p) { + const tPromise = new winjs.Promise((c, e) => { resolveTPromise = c; }); @@ -28,7 +28,7 @@ suite('WinJS and ES6 Promises', function () { test('new Promise', function () { let resolveTPromise; - const tPromise = new winjs.Promise(function (c, e, p) { + const tPromise = new winjs.Promise((c, e) => { resolveTPromise = c; }); diff --git a/src/vs/base/test/node/config.test.ts b/src/vs/base/test/node/config.test.ts index 8a0002bc406..8be86fd083c 100644 --- a/src/vs/base/test/node/config.test.ts +++ b/src/vs/base/test/node/config.test.ts @@ -77,70 +77,70 @@ suite('Config', () => { }); }); - test('watching', function (done) { - this.timeout(10000); // watching is timing intense + // test('watching', function (done) { + // this.timeout(10000); // watching is timing intense - testFile('config', 'config.json').then(res => { - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); + // testFile('config', 'config.json').then(res => { + // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); - let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - watcher.getConfig(); // ensure we are in sync + // let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); + // watcher.getConfig(); // ensure we are in sync - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); + // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); - watcher.onDidUpdateConfiguration(event => { - assert.ok(event); - assert.equal(event.config.foo, 'changed'); - assert.equal(watcher.getValue('foo'), 'changed'); + // watcher.onDidUpdateConfiguration(event => { + // assert.ok(event); + // assert.equal(event.config.foo, 'changed'); + // assert.equal(watcher.getValue('foo'), 'changed'); - watcher.dispose(); + // watcher.dispose(); - res.cleanUp().then(done, done); - }); - }, done); - }); + // res.cleanUp().then(done, done); + // }); + // }, done); + // }); - test('watching also works when file created later', function (done) { - this.timeout(10000); // watching is timing intense + // test('watching also works when file created later', function (done) { + // this.timeout(10000); // watching is timing intense - testFile('config', 'config.json').then(res => { - let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - watcher.getConfig(); // ensure we are in sync + // testFile('config', 'config.json').then(res => { + // let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); + // watcher.getConfig(); // ensure we are in sync - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); + // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "changed" }'); - watcher.onDidUpdateConfiguration(event => { - assert.ok(event); - assert.equal(event.config.foo, 'changed'); - assert.equal(watcher.getValue('foo'), 'changed'); + // watcher.onDidUpdateConfiguration(event => { + // assert.ok(event); + // assert.equal(event.config.foo, 'changed'); + // assert.equal(watcher.getValue('foo'), 'changed'); - watcher.dispose(); + // watcher.dispose(); - res.cleanUp().then(done, done); - }); - }, done); - }); + // res.cleanUp().then(done, done); + // }); + // }, done); + // }); - test('watching detects the config file getting deleted', function (done) { - this.timeout(10000); // watching is timing intense + // test('watching detects the config file getting deleted', function (done) { + // this.timeout(10000); // watching is timing intense - testFile('config', 'config.json').then(res => { - fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); + // testFile('config', 'config.json').then(res => { + // fs.writeFileSync(res.testFile, '// my comment\n{ "foo": "bar" }'); - let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); - watcher.getConfig(); // ensure we are in sync + // let watcher = new ConfigWatcher<{ foo: string; }>(res.testFile); + // watcher.getConfig(); // ensure we are in sync - watcher.onDidUpdateConfiguration(event => { - assert.ok(event); + // watcher.onDidUpdateConfiguration(event => { + // assert.ok(event); - watcher.dispose(); + // watcher.dispose(); - res.cleanUp().then(done, done); - }); + // res.cleanUp().then(done, done); + // }); - fs.unlinkSync(res.testFile); - }, done); - }); + // fs.unlinkSync(res.testFile); + // }, done); + // }); test('reload', function (done) { testFile('config', 'config.json').then(res => { diff --git a/src/vs/code/electron-browser/issue/issueReporter.js b/src/vs/code/electron-browser/issue/issueReporter.js index 5b246f9491b..53abd748d7b 100644 --- a/src/vs/code/electron-browser/issue/issueReporter.js +++ b/src/vs/code/electron-browser/issue/issueReporter.js @@ -45,6 +45,8 @@ function readFile(file) { }); } +const writeFile = (file, content) => new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); + function main() { const args = parseURLQueryArgs(); const configuration = JSON.parse(args['config'] || '{}') || {}; @@ -127,8 +129,15 @@ function main() { let json = JSON.parse(content); bundles[bundle] = json; cb(undefined, json); - }) - .catch(cb); + }).catch((error) => { + try { + if (nlsConfig._corruptedFile) { + writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + } + } finally { + cb(error, undefined); + } + }); }; } diff --git a/src/vs/code/electron-browser/processExplorer/processExplorer.js b/src/vs/code/electron-browser/processExplorer/processExplorer.js index 1fdb4bb8296..0a798fb876d 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorer.js +++ b/src/vs/code/electron-browser/processExplorer/processExplorer.js @@ -45,6 +45,8 @@ function readFile(file) { }); } +const writeFile = (file, content) => new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); + function main() { const args = parseURLQueryArgs(); const configuration = JSON.parse(args['config'] || '{}') || {}; @@ -102,8 +104,15 @@ function main() { let json = JSON.parse(content); bundles[bundle] = json; cb(undefined, json); - }) - .catch(cb); + }).catch((error) => { + try { + if (nlsConfig._corruptedFile) { + writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + } + } finally { + cb(error, undefined); + } + }); }; } diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 98d6e852940..d7dd08b4e3d 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -53,6 +53,8 @@ function readFile(file) { }); } +const writeFile = (file, content) => new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); + function main() { const args = parseURLQueryArgs(); const configuration = JSON.parse(args['config'] || '{}') || {}; @@ -111,8 +113,15 @@ function main() { let json = JSON.parse(content); bundles[bundle] = json; cb(undefined, json); - }) - .catch(cb); + }).catch((error) => { + try { + if (nlsConfig._corruptedFile) { + writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + } + } finally { + cb(error, undefined); + } + }); }; } diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 50027d9528e..deaea3955f3 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -43,6 +43,7 @@ import { LocalizationsChannel } from 'vs/platform/localizations/common/localizat import { DialogChannelClient } from 'vs/platform/dialogs/common/dialogIpc'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DefaultURITransformer } from 'vs/base/common/uriIpc'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -66,7 +67,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I process.once('exit', () => dispose(disposables)); const environmentService = new EnvironmentService(initData.args, process.execPath); - const logLevelClient = new LogLevelSetterChannelClient(server.getChannel('loglevel', { route: () => 'main' })); + const logLevelClient = new LogLevelSetterChannelClient(server.getChannel('loglevel', { routeCall: () => 'main', routeEvent: () => 'main' })); const logService = new FollowerLogService(logLevelClient, createSpdLogService('sharedprocess', initData.logLevel, environmentService.logsPath)); disposables.push(logService); @@ -77,14 +78,18 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); services.set(IRequestService, new SyncDescriptor(RequestService)); - const windowsChannel = server.getChannel('windows', { route: () => 'main' }); + const windowsChannel = server.getChannel('windows', { routeCall: () => 'main', routeEvent: () => 'main' }); const windowsService = new WindowsChannelClient(windowsChannel); services.set(IWindowsService, windowsService); const activeWindowManager = new ActiveWindowManager(windowsService); const dialogChannel = server.getChannel('dialog', { - route: () => { - logService.info('Routing dialog request to the client', activeWindowManager.activeClientId); + routeCall: () => { + logService.info('Routing dialog call request to the client', activeWindowManager.activeClientId); + return activeWindowManager.activeClientId; + }, + routeEvent: () => { + logService.info('Routing dialog listen request to the client', activeWindowManager.activeClientId); return activeWindowManager.activeClientId; } }); @@ -130,7 +135,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I instantiationService2.invokeFunction(accessor => { const extensionManagementService = accessor.get(IExtensionManagementService); - const channel = new ExtensionManagementChannel(extensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, DefaultURITransformer); server.registerChannel('extensions', channel); // clean up deprecated extensions diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index edd6a1a8f68..f2a7d0cae04 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -99,6 +99,7 @@ export class CodeApplication { // We handle uncaught exceptions here to prevent electron from opening a dialog to the user errors.setUnexpectedErrorHandler(err => this.onUnexpectedError(err)); process.on('uncaughtException', err => this.onUnexpectedError(err)); + process.on('unhandledRejection', (reason: any, promise: Promise) => errors.onUnexpectedError(reason)); app.on('will-quit', () => { this.logService.trace('App#will-quit: disposing resources'); @@ -425,7 +426,8 @@ export class CodeApplication { // Create a URL handler which forwards to the last active window const activeWindowManager = new ActiveWindowManager(windowsService); - const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { route: () => activeWindowManager.activeClientId }); + const route = () => activeWindowManager.activeClientId; + const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { routeCall: route, routeEvent: route }); const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); // On Mac, Code can be running without any open windows, so we must create a window to handle urls, @@ -434,7 +436,7 @@ export class CodeApplication { const environmentService = accessor.get(IEnvironmentService); urlService.registerHandler({ - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { if (windowsMainService.getWindowCount() === 0) { const cli = { ...environmentService.args, goto: true }; const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true }); @@ -442,7 +444,7 @@ export class CodeApplication { return window.ready().then(() => urlService.open(uri)); } - return false; + return TPromise.as(false); } }); } diff --git a/src/vs/code/electron-main/launch.ts b/src/vs/code/electron-main/launch.ts index 7da8d2edbd2..d1781a1a89b 100644 --- a/src/vs/code/electron-main/launch.ts +++ b/src/vs/code/electron-main/launch.ts @@ -20,6 +20,7 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import URI from 'vs/base/common/uri'; import { BrowserWindow } from 'electron'; +import { Event } from 'vs/base/common/event'; export const ID = 'launchService'; export const ILaunchService = createDecorator(ID); @@ -79,6 +80,10 @@ export class LaunchChannel implements ILaunchChannel { constructor(private service: ILaunchService) { } + listen(event: string): Event { + throw new Error('No event found'); + } + call(command: string, arg: any): TPromise { switch (command) { case 'start': diff --git a/src/vs/code/electron-main/logUploader.ts b/src/vs/code/electron-main/logUploader.ts index cf76bbbcd1b..7142aa19a4c 100644 --- a/src/vs/code/electron-main/logUploader.ts +++ b/src/vs/code/electron-main/logUploader.ts @@ -37,7 +37,7 @@ export async function uploadLogs( channel: ILaunchChannel, requestService: IRequestService, environmentService: IEnvironmentService -): TPromise { +): Promise { const endpoint = Endpoint.getFromProduct(); if (!endpoint) { console.error(localize('invalidEndpoint', 'Invalid log uploader endpoint')); @@ -75,7 +75,7 @@ async function postLogs( endpoint: Endpoint, outZip: string, requestService: IRequestService -): TPromise { +): Promise { const dotter = setInterval(() => console.log('.'), 5000); let result: IRequestContext; try { @@ -152,4 +152,4 @@ function doZip( default: return cp.execFile('zip', ['-r', outZip, '.'], { cwd: logsPath }, callback); } -} \ No newline at end of file +} diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 50735d5f45a..1cc94b2b298 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -240,9 +240,9 @@ export class CodeMenu { this.setGotoMenu(gotoMenu); // Terminal - // const terminalMenu = new Menu(); - // const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); - // this.setTerminalMenu(terminalMenu); + const terminalMenu = new Menu(); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "Ter&&minal")), submenu: terminalMenu }); + this.setTerminalMenu(terminalMenu); // Debug const debugMenu = new Menu(); @@ -277,7 +277,7 @@ export class CodeMenu { menubar.append(selectionMenuItem); menubar.append(viewMenuItem); menubar.append(gotoMenuItem); - // menubar.append(terminalMenuItem); + menubar.append(terminalMenuItem); menubar.append(debugMenuItem); menubar.append(taskMenuItem); @@ -385,7 +385,8 @@ export class CodeMenu { const saveAllFiles = this.createMenuItem(nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), 'workbench.action.files.saveAll'); const autoSaveEnabled = [AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => this.currentAutoSaveSetting === s); - const autoSave = new MenuItem(this.likeAction('vscode.toggleAutoSave', { label: this.mnemonicLabel(nls.localize('miAutoSave', "Auto Save")), type: 'checkbox', checked: autoSaveEnabled, enabled: this.windowsMainService.getWindowCount() > 0, click: () => this.windowsMainService.sendToFocused('vscode.toggleAutoSave') }, false)); + + const autoSave = this.createMenuItem(this.mnemonicLabel(nls.localize('miAutoSave', "Auto Save")), 'workbench.action.toggleAutoSave', this.windowsMainService.getWindowCount() > 0, autoSaveEnabled); const preferences = this.getPreferencesMenu(); @@ -658,7 +659,7 @@ export class CodeMenu { // Panels const output = this.createMenuItem(nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output"), 'workbench.action.output.toggleOutput'); const debugConsole = this.createMenuItem(nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console"), 'workbench.debug.action.toggleRepl'); - const integratedTerminal = this.createMenuItem(nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Integrated Terminal"), 'workbench.action.terminal.toggleTerminal'); + const terminal = this.createMenuItem(nls.localize({ key: 'miToggleTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), 'workbench.action.terminal.toggleTerminal'); const problems = this.createMenuItem(nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems"), 'workbench.actions.view.problems'); const fullscreen = new MenuItem(this.withKeybinding('workbench.action.toggleFullScreen', { label: this.mnemonicLabel(nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen")), click: () => this.windowsMainService.getLastActiveWindow().toggleFullScreen(), enabled: this.windowsMainService.getWindowCount() > 0 })); @@ -681,7 +682,7 @@ export class CodeMenu { const twoRowsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows"), 'workbench.action.editorLayoutTwoRows'); const threeRowsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows"), 'workbench.action.editorLayoutThreeRows'); const twoByTwoGridEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)"), 'workbench.action.editorLayoutTwoByTwoGrid'); - const twoColumnsRightEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two C&&olumns Right"), 'workbench.action.editorLayoutTwoColumnsRight'); + const twoRowsRightEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoRowsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two R&&ows Right"), 'workbench.action.editorLayoutTwoRowsRight'); const twoColumnsBottomEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom"), 'workbench.action.editorLayoutTwoColumnsBottom'); const toggleEditorLayout = this.createMenuItem(nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Toggle Vertical/Horizontal &&Layout"), 'workbench.action.toggleEditorGroupLayout'); @@ -698,7 +699,7 @@ export class CodeMenu { twoRowsEditorLayout, threeRowsEditorLayout, twoByTwoGridEditorLayout, - twoColumnsRightEditorLayout, + twoRowsRightEditorLayout, twoColumnsBottomEditorLayout, __separator__(), toggleEditorLayout @@ -759,7 +760,7 @@ export class CodeMenu { output, problems, debugConsole, - integratedTerminal, + terminal, __separator__(), fullscreen, toggleZenMode, @@ -864,71 +865,35 @@ export class CodeMenu { ].forEach(item => gotoMenu.append(item)); } - // private setTerminalMenu(terminalMenu: Electron.Menu): void { - // const newTerminal = this.createMenuItem(nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal"), 'workbench.action.terminal.new'); - // const splitTerminal = this.createMenuItem(nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"), 'workbench.action.terminal.split'); - // const killTerminal = this.createMenuItem(nls.localize({ key: 'miKillTerminal', comment: ['&& denotes a mnemonic'] }, "&&Kill Terminal"), 'workbench.action.terminal.kill'); + private setTerminalMenu(terminalMenu: Electron.Menu): void { + const newTerminal = this.createMenuItem(nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal"), 'workbench.action.terminal.new'); + const splitTerminal = this.createMenuItem(nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"), 'workbench.action.terminal.split'); + const killTerminal = this.createMenuItem(nls.localize({ key: 'miKillTerminal', comment: ['&& denotes a mnemonic'] }, "&&Kill Terminal"), 'workbench.action.terminal.kill'); + const clear = this.createMenuItem(nls.localize({ key: 'miClear', comment: ['&& denotes a mnemonic'] }, "&&Clear"), 'workbench.action.terminal.clear'); + const runActiveFile = this.createMenuItem(nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File"), 'workbench.action.terminal.runActiveFile'); + const runSelectedText = this.createMenuItem(nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text"), 'workbench.action.terminal.runSelectedText'); + const scrollToPreviousCommand = this.createMenuItem(nls.localize({ key: 'miScrollToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Previous Command"), 'workbench.action.terminal.scrollToPreviousCommand'); + const scrollToNextCommand = this.createMenuItem(nls.localize({ key: 'miScrollToNextCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Next Command"), 'workbench.action.terminal.scrollToNextCommand'); + const selectToPreviousCommand = this.createMenuItem(nls.localize({ key: 'miSelectToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Select To Previous Command"), 'workbench.action.terminal.selectToPreviousCommand'); + const selectToNextCommand = this.createMenuItem(nls.localize({ key: 'miSelectToNextCommand', comment: ['&& denotes a mnemonic'] }, "Select To Next Command"), 'workbench.action.terminal.selectToNextCommand'); - // const clear = this.createMenuItem(nls.localize({ key: 'miClear', comment: ['&& denotes a mnemonic'] }, "&&Clear"), 'workbench.action.terminal.clear'); - // // const deleteWordLeft = this.createMenuItem(nls.localize({ key: 'miDeleteWordLeft', comment: ['&& denotes a mnemonic'] }, "Delete Word To &&Left"), 'workbench.action.terminal.deleteWordLeft'); - // // const deleteWordRight = this.createMenuItem(nls.localize({ key: 'miDeleteWordRight', comment: ['&& denotes a mnemonic'] }, "Delete Word To &&Right"), 'workbench.action.terminal.deleteWordRight'); - // // const moveToLineStart = this.createMenuItem(nls.localize({ key: 'miMoveToLineStart', comment: ['&& denotes a mnemonic'] }, "Move to Line Start"), 'workbench.action.terminal.moveToLineStart'); - // // const moveToLineEnd = this.createMenuItem(nls.localize({ key: 'miMoveToLineEnd', comment: ['&& denotes a mnemonic'] }, "Move to Line &&End"), 'workbench.action.terminal.moveToLineEnd'); + const menuItems: MenuItem[] = [ + newTerminal, + splitTerminal, + killTerminal, + __separator__(), + clear, + runActiveFile, + runSelectedText, + __separator__(), + scrollToPreviousCommand, + scrollToNextCommand, + selectToPreviousCommand, + selectToNextCommand + ]; - // const runActiveFile = this.createMenuItem(nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File"), 'workbench.action.terminal.runActiveFile'); - // const runSelectedText = this.createMenuItem(nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text"), 'workbench.action.terminal.runSelectedText'); - - // // const scrollUp = this.createMenuItem(nls.localize({ key: 'miScrollUp', comment: ['&& denotes a mnemonic'] }, "Scroll Up"), 'workbench.action.terminal.scrollUp'); - // // const scrollDown = this.createMenuItem(nls.localize({ key: 'miScrollDown', comment: ['&& denotes a mnemonic'] }, "Scroll Down"), 'workbench.action.terminal.scrollDown'); - // // const scrollUpPage = this.createMenuItem(nls.localize({ key: 'miScrollUpPage', comment: ['&& denotes a mnemonic'] }, "Scroll Up Page"), 'workbench.action.terminal.scrollUpPage'); - // // const scrollDownPage = this.createMenuItem(nls.localize({ key: 'miScrollDownPage', comment: ['&& denotes a mnemonic'] }, "Scroll Down Page"), 'workbench.action.terminal.scrollDownPage'); - // // const scrollToTop = this.createMenuItem(nls.localize({ key: 'miScrollToTop', comment: ['&& denotes a mnemonic'] }, "Scroll To Top"), 'workbench.action.terminal.scrollToTop'); - // // const scrollToBottom = this.createMenuItem(nls.localize({ key: 'miScrollToBottom', comment: ['&& denotes a mnemonic'] }, "Scroll To Bottom"), 'workbench.action.terminal.scrollToBottom'); - // const scrollToPreviousCommand = this.createMenuItem(nls.localize({ key: 'miScrollToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Previous Command"), 'workbench.action.terminal.scrollToPreviousCommand'); - // const scrollToNextCommand = this.createMenuItem(nls.localize({ key: 'miScrollToNextCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Next Command"), 'workbench.action.terminal.scrollToNextCommand'); - - // const selectToPreviousCommand = this.createMenuItem(nls.localize({ key: 'miSelectToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Select To Previous Command"), 'workbench.action.terminal.selectToPreviousCommand'); - // const selectToNextCommand = this.createMenuItem(nls.localize({ key: 'miSelectToNextCommand', comment: ['&& denotes a mnemonic'] }, "Select To Next Command"), 'workbench.action.terminal.selectToNextCommand'); - - // const menuItems: MenuItem[] = [ - // newTerminal, - // splitTerminal, - // killTerminal, - // __separator__(), - // clear, - // ]; - // // if (!isWindows) { - // // menuItems.push( - // // deleteWordLeft, - // // deleteWordRight, - // // ); - - // // } - // // if (isMacintosh) { - // // menuItems.push( - // // moveToLineStart, - // // moveToLineEnd - // // ); - // // } - // menuItems.push( - // runActiveFile, - // runSelectedText, - // __separator__(), - // // scrollUp, - // // scrollDown, - // // scrollUpPage, - // // scrollDownPage, - // // scrollToTop, - // // scrollToBottom, - // scrollToPreviousCommand, - // scrollToNextCommand, - // // __separator__(), - // selectToPreviousCommand, - // selectToNextCommand - // ); - - // menuItems.forEach(item => terminalMenu.append(item)); - // } + menuItems.forEach(item => terminalMenu.append(item)); + } private setDebugMenu(debugMenu: Electron.Menu): void { const start = this.createMenuItem(nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging"), 'workbench.action.debug.start'); diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 1ab88ea05ce..9774ab0405b 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -573,6 +573,11 @@ const editorConfiguration: IConfigurationNode = { default: true, description: nls.localize('suggest.filterGraceful', "Controls whether filtering and sorting suggestions accounts for small typos.") }, + 'editor.suggest.snippetsPreventQuickSuggestions': { + type: 'boolean', + default: true, + description: nls.localize('suggest.snippetsPreventQuickSuggestions', "Control whether an active snippet prevents quick suggestions.") + }, 'editor.selectionHighlight': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.selectionHighlight, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2189eaf348e..14365ee9925 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -163,6 +163,10 @@ export interface ISuggestOptions { * Enable graceful matching. Defaults to true. */ filterGraceful?: boolean; + /** + * Prevent quick suggestions when a snippet is active. Defaults to true. + */ + snippetsPreventQuickSuggestions?: boolean; } /** @@ -397,7 +401,7 @@ export interface IEditorOptions { /** * Configure the editor's hover. */ - hover?: boolean | IEditorHoverOptions; + hover?: IEditorHoverOptions; /** * Enable detecting links and making them clickable. * Defaults to true. @@ -844,6 +848,7 @@ export interface InternalEditorHoverOptions { export interface InternalSuggestOptions { readonly filterGraceful: boolean; readonly snippets: 'top' | 'bottom' | 'inline' | 'none'; + readonly snippetsPreventQuickSuggestions: boolean; } export interface EditorWrappingInfo { @@ -1252,7 +1257,9 @@ export class InternalEditorOptions { } else if (!a || !b) { return false; } else { - return a.filterGraceful === b.filterGraceful && a.snippets === b.snippets; + return a.filterGraceful === b.filterGraceful + && a.snippets === b.snippets + && a.snippetsPreventQuickSuggestions === b.snippetsPreventQuickSuggestions; } } @@ -1751,6 +1758,7 @@ export class EditorOptionsValidator { return { filterGraceful: _boolean(opts.suggest.filterGraceful, defaults.filterGraceful), snippets: _stringSet<'top' | 'bottom' | 'inline' | 'none'>(opts.snippetSuggestions, defaults.snippets, ['top', 'bottom', 'inline', 'none']), + snippetsPreventQuickSuggestions: _boolean(opts.suggest.snippetsPreventQuickSuggestions, defaults.filterGraceful), }; } @@ -2470,7 +2478,8 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { suggestLineHeight: 0, suggest: { filterGraceful: true, - snippets: 'inline' + snippets: 'inline', + snippetsPreventQuickSuggestions: true }, selectionHighlight: true, occurrencesHighlight: true, diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 53dc47f82e1..1a4005d6a1b 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -46,7 +46,7 @@ export function createTextBufferFactory(text: string): model.ITextBufferFactory } export function createTextBufferFactoryFromStream(stream: IStringStream, filter?: (chunk: string) => string): TPromise { - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { let done = false; let builder = createTextBufferBuilder(); diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index c13ef19a29d..9a6ea89f441 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -7,7 +7,6 @@ import { isFalsyOrEmpty, mergeSort, flatten } from 'vs/base/common/arrays'; import { asWinJsPromise } from 'vs/base/common/async'; import { illegalArgument, onUnexpectedExternalError, isPromiseCanceledError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; @@ -15,8 +14,9 @@ import { CodeAction, CodeActionProviderRegistry, CodeActionContext, CodeActionTr import { IModelService } from 'vs/editor/common/services/modelService'; import { CodeActionFilter, CodeActionKind, CodeActionTrigger } from './codeActionTrigger'; import { Selection } from 'vs/editor/common/core/selection'; +import { CancellationToken } from 'vs/base/common/cancellation'; -export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, trigger?: CodeActionTrigger): TPromise { +export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, trigger?: CodeActionTrigger, token: CancellationToken = CancellationToken.None): Promise { const codeActionContext: CodeActionContext = { only: trigger && trigger.filter && trigger.filter.kind ? trigger.filter.kind.value : undefined, trigger: trigger && trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic @@ -38,7 +38,7 @@ export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Sele }); }); - return TPromise.join(promises) + return Promise.all(promises) .then(flatten) .then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator)); } @@ -88,5 +88,5 @@ registerLanguageCommand('_executeCodeActionProvider', function (accessor, args) throw illegalArgument(); } - return getCodeActions(model, model.validateRange(range), undefined); + return getCodeActions(model, model.validateRange(range), { type: 'manual', filter: { includeSourceActions: true } }); }); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 8af707d45ba..c86a8b15608 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -25,6 +25,7 @@ import { LightBulbWidget } from './lightBulbWidget'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IProgressService } from 'vs/platform/progress/common/progress'; +import { CancelablePromise } from 'vs/base/common/async'; function contextKeyForSupportedActions(kind: CodeActionKind) { return ContextKeyExpr.regex( @@ -46,7 +47,7 @@ export class QuickFixController implements IEditorContribution { private _lightBulbWidget: LightBulbWidget; private _disposables: IDisposable[] = []; - private _activeRequest: TPromise | undefined; + private _activeRequest: CancelablePromise | undefined; constructor(editor: ICodeEditor, @IMarkerService markerService: IMarkerService, @@ -124,7 +125,7 @@ export class QuickFixController implements IEditorContribution { this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords); } - public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): TPromise { + public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): Thenable { return this._model.trigger({ type: 'manual', filter, autoApply }); } diff --git a/src/vs/editor/contrib/codeAction/codeActionModel.ts b/src/vs/editor/contrib/codeAction/codeActionModel.ts index acbd68b1fa1..98211ae24dd 100644 --- a/src/vs/editor/contrib/codeAction/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/codeActionModel.ts @@ -17,6 +17,7 @@ import { IMarkerService } from 'vs/platform/markers/common/markers'; import { getCodeActions } from './codeAction'; import { CodeActionTrigger } from './codeActionTrigger'; import { IProgressService } from 'vs/platform/progress/common/progress'; +import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; export const SUPPORTED_CODE_ACTIONS = new RawContextKey('supportedCodeAction', ''); @@ -99,7 +100,7 @@ export class CodeActionOracle { return selection; } - private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): TPromise { + private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): Thenable { if (!selection) { // cancel this._signalChange({ @@ -113,10 +114,10 @@ export class CodeActionOracle { const model = this._editor.getModel(); const markerRange = this._getRangeOfMarker(selection); const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition(); - const actions = getCodeActions(model, selection, trigger); + const actions = createCancelablePromise(token => getCodeActions(model, selection, trigger, token)); if (this._progressService && trigger.type === 'manual') { - this._progressService.showWhile(actions, 250); + this._progressService.showWhile(TPromise.wrap(actions), 250); } this._signalChange({ @@ -134,7 +135,7 @@ export interface CodeActionsComputeEvent { trigger: CodeActionTrigger; rangeOrSelection: Range | Selection; position: Position; - actions: TPromise; + actions: CancelablePromise; } export class CodeActionModel { @@ -196,7 +197,7 @@ export class CodeActionModel { } } - trigger(trigger: CodeActionTrigger): TPromise { + trigger(trigger: CodeActionTrigger): Thenable { if (this._codeActionOracle) { return this._codeActionOracle.trigger(trigger); } diff --git a/src/vs/editor/contrib/codeAction/codeActionWidget.ts b/src/vs/editor/contrib/codeAction/codeActionWidget.ts index 0020eca69a4..b680ed6008b 100644 --- a/src/vs/editor/contrib/codeAction/codeActionWidget.ts +++ b/src/vs/editor/contrib/codeAction/codeActionWidget.ts @@ -28,7 +28,7 @@ export class CodeActionContextMenu { private readonly _onApplyCodeAction: (action: CodeAction) => TPromise ) { } - show(fixes: TPromise, at: { x: number; y: number } | Position) { + show(fixes: Thenable, at: { x: number; y: number } | Position) { const actions = fixes.then(value => { return value.map(action => { @@ -53,7 +53,7 @@ export class CodeActionContextMenu { } return at; }, - getActions: () => actions, + getActions: () => TPromise.wrap(actions), onHide: () => { this._visible = false; this._editor.focus(); diff --git a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts index fd77972c961..0fd9d8270f1 100644 --- a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts @@ -116,7 +116,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { this._model = value; const selection = this._model.rangeOrSelection; - this._model.actions.done(fixes => { + this._model.actions.then(fixes => { if (!token.isCancellationRequested && fixes && fixes.length > 0) { if (selection.isEmpty() && fixes.every(fix => fix.kind && CodeActionKind.Refactor.contains(fix.kind))) { this.hide(); @@ -126,7 +126,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { } else { this.hide(); } - }, err => { + }).catch(err => { this.hide(); }); } diff --git a/src/vs/editor/contrib/codelens/codelens.ts b/src/vs/editor/contrib/codelens/codelens.ts index 92821ec5ee6..9c58affec6a 100644 --- a/src/vs/editor/contrib/codelens/codelens.ts +++ b/src/vs/editor/contrib/codelens/codelens.ts @@ -8,12 +8,10 @@ import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { mergeSort } from 'vs/base/common/arrays'; import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { asWinJsPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; export interface ICodeLensData { @@ -21,20 +19,20 @@ export interface ICodeLensData { provider: CodeLensProvider; } -export function getCodeLensData(model: ITextModel): TPromise { +export function getCodeLensData(model: ITextModel, token: CancellationToken): Promise { const symbols: ICodeLensData[] = []; const provider = CodeLensProviderRegistry.ordered(model); - const promises = provider.map(provider => asWinJsPromise(token => provider.provideCodeLenses(model, token)).then(result => { + const promises = provider.map(provider => Promise.resolve(provider.provideCodeLenses(model, token)).then(result => { if (Array.isArray(result)) { for (let symbol of result) { symbols.push({ symbol, provider }); } } - }, onUnexpectedExternalError)); + }).catch(onUnexpectedExternalError)); - return TPromise.join(promises).then(() => { + return Promise.all(promises).then(() => { return mergeSort(symbols, (a, b) => { // sort by lineNumber, provider-rank, and column @@ -70,7 +68,7 @@ registerLanguageCommand('_executeCodeLensProvider', function (accessor, args) { } const result: ICodeLensSymbol[] = []; - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { let resolve: Thenable[] = []; diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 0cd0fb98162..be43648e661 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -5,10 +5,9 @@ 'use strict'; -import { RunOnceScheduler, asWinJsPromise } from 'vs/base/common/async'; +import { RunOnceScheduler, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes'; @@ -30,9 +29,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { private _globalToDispose: IDisposable[]; private _localToDispose: IDisposable[]; private _lenses: CodeLens[]; - private _currentFindCodeLensSymbolsPromise: TPromise; + private _currentFindCodeLensSymbolsPromise: CancelablePromise; private _modelChangeCounter: number; - private _currentFindOccPromise: TPromise; + private _currentResolveCodeLensSymbolsPromise: CancelablePromise; private _detectVisibleLenses: RunOnceScheduler; constructor( @@ -72,9 +71,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._currentFindCodeLensSymbolsPromise = null; this._modelChangeCounter++; } - if (this._currentFindOccPromise) { - this._currentFindOccPromise.cancel(); - this._currentFindOccPromise = null; + if (this._currentResolveCodeLensSymbolsPromise) { + this._currentResolveCodeLensSymbolsPromise.cancel(); + this._currentResolveCodeLensSymbolsPromise = null; } this._localToDispose = dispose(this._localToDispose); } @@ -117,7 +116,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._currentFindCodeLensSymbolsPromise.cancel(); } - this._currentFindCodeLensSymbolsPromise = getCodeLensData(model); + this._currentFindCodeLensSymbolsPromise = createCancelablePromise(token => getCodeLensData(model, token)); this._currentFindCodeLensSymbolsPromise.then((result) => { if (counterValue === this._modelChangeCounter) { // only the last one wins @@ -267,9 +266,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { } private _onViewportChanged(): void { - if (this._currentFindOccPromise) { - this._currentFindOccPromise.cancel(); - this._currentFindOccPromise = null; + if (this._currentResolveCodeLensSymbolsPromise) { + this._currentResolveCodeLensSymbolsPromise.cancel(); + this._currentResolveCodeLensSymbolsPromise = null; } const model = this._editor.getModel(); @@ -291,28 +290,34 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { return; } - const promises = toResolve.map((request, i) => { + this._currentResolveCodeLensSymbolsPromise = createCancelablePromise(token => { - const resolvedSymbols = new Array(request.length); - const promises = request.map((request, i) => { - if (typeof request.provider.resolveCodeLens === 'function') { - return asWinJsPromise((token) => { - return request.provider.resolveCodeLens(model, request.symbol, token); - }).then(symbol => { - resolvedSymbols[i] = symbol; - }); - } - resolvedSymbols[i] = request.symbol; - return TPromise.as(void 0); + const promises = toResolve.map((request, i) => { + + const resolvedSymbols = new Array(request.length); + const promises = request.map((request, i) => { + if (typeof request.provider.resolveCodeLens === 'function') { + return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => { + resolvedSymbols[i] = symbol; + }); + } + resolvedSymbols[i] = request.symbol; + return Promise.resolve(void 0); + }); + + return Promise.all(promises).then(() => { + lenses[i].updateCommands(resolvedSymbols); + }); }); - return TPromise.join(promises).then(() => { - lenses[i].updateCommands(resolvedSymbols); - }); + return Promise.all(promises); }); - this._currentFindOccPromise = TPromise.join(promises).then(() => { - this._currentFindOccPromise = null; + this._currentResolveCodeLensSymbolsPromise.then(() => { + this._currentResolveCodeLensSymbolsPromise = null; + }).catch(err => { + this._currentResolveCodeLensSymbolsPromise = null; + onUnexpectedError(err); }); } } diff --git a/src/vs/editor/contrib/colorPicker/color.ts b/src/vs/editor/contrib/colorPicker/color.ts index e3f8d687510..2ebc3d1b8ca 100644 --- a/src/vs/editor/contrib/colorPicker/color.ts +++ b/src/vs/editor/contrib/colorPicker/color.ts @@ -13,6 +13,7 @@ import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range, IRange } from 'vs/editor/common/core/range'; import { illegalArgument } from 'vs/base/common/errors'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IColorData { @@ -20,10 +21,10 @@ export interface IColorData { provider: DocumentColorProvider; } -export function getColors(model: ITextModel): TPromise { +export function getColors(model: ITextModel, token: CancellationToken): Promise { const colors: IColorData[] = []; const providers = ColorProviderRegistry.ordered(model).reverse(); - const promises = providers.map(provider => asWinJsPromise(token => provider.provideDocumentColors(model, token)).then(result => { + const promises = providers.map(provider => Promise.resolve(provider.provideDocumentColors(model, token)).then(result => { if (Array.isArray(result)) { for (let colorInfo of result) { colors.push({ colorInfo, provider }); @@ -31,11 +32,11 @@ export function getColors(model: ITextModel): TPromise { } })); - return TPromise.join(promises).then(() => colors); + return Promise.all(promises).then(() => colors); } -export function getColorPresentations(model: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider): TPromise { - return asWinJsPromise(token => provider.provideColorPresentations(model, colorInfo, token)); +export function getColorPresentations(model: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider, token: CancellationToken): Promise { + return Promise.resolve(provider.provideColorPresentations(model, colorInfo, token)); } registerLanguageCommand('_executeDocumentColorProvider', function (accessor, args) { diff --git a/src/vs/editor/contrib/colorPicker/colorDetector.ts b/src/vs/editor/contrib/colorPicker/colorDetector.ts index 97fbb7b8852..b3836d6aa68 100644 --- a/src/vs/editor/contrib/colorPicker/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/colorDetector.ts @@ -6,7 +6,6 @@ import { RGBA } from 'vs/base/common/color'; import { hash } from 'vs/base/common/hash'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -17,6 +16,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { getColors, IColorData } from 'vs/editor/contrib/colorPicker/color'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; const MAX_DECORATORS = 500; @@ -28,8 +28,8 @@ export class ColorDetector implements IEditorContribution { private _globalToDispose: IDisposable[] = []; private _localToDispose: IDisposable[] = []; - private _computePromise: TPromise; - private _timeoutPromise: TPromise; + private _computePromise: CancelablePromise; + private _timeoutTimer: TimeoutTimer; private _decorationsIds: string[] = []; private _colorDatas = new Map(); @@ -61,7 +61,7 @@ export class ColorDetector implements IEditorContribution { } })); - this._timeoutPromise = null; + this._timeoutTimer = null; this._computePromise = null; this._isEnabled = this.isEnabled(); this.onModelChanged(); @@ -115,29 +115,31 @@ export class ColorDetector implements IEditorContribution { } this._localToDispose.push(this._editor.onDidChangeModelContent((e) => { - if (!this._timeoutPromise) { - this._timeoutPromise = TPromise.timeout(ColorDetector.RECOMPUTE_TIME); - this._timeoutPromise.then(() => { - this._timeoutPromise = null; + if (!this._timeoutTimer) { + this._timeoutTimer = new TimeoutTimer(); + this._timeoutTimer.cancelAndSet(() => { + this._timeoutTimer = null; this.beginCompute(); - }); + }, ColorDetector.RECOMPUTE_TIME); } })); this.beginCompute(); } private beginCompute(): void { - this._computePromise = getColors(this._editor.getModel()).then(colorInfos => { - this.updateDecorations(colorInfos); - this.updateColorDecorators(colorInfos); - this._computePromise = null; + this._computePromise = createCancelablePromise(token => { + return getColors(this._editor.getModel(), token).then(colorInfos => { + this.updateDecorations(colorInfos); + this.updateColorDecorators(colorInfos); + this._computePromise = null; + }); }); } private stop(): void { - if (this._timeoutPromise) { - this._timeoutPromise.cancel(); - this._timeoutPromise = null; + if (this._timeoutTimer) { + this._timeoutTimer.cancel(); + this._timeoutTimer = null; } if (this._computePromise) { this._computePromise.cancel(); diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index abdc0f2abd0..a3467f32a75 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -142,7 +142,9 @@ export class ContextMenuController implements IEditorContribution { // Disable hover const oldHoverSetting = this._editor.getConfiguration().contribInfo.hover; this._editor.updateOptions({ - hover: false + hover: { + enabled: false + } }); let menuPosition = forcedPosition; diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css new file mode 100644 index 00000000000..f62841df77d --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-tree.focused .selected .outline-element-label, .monaco-tree.focused .selected .outline-element-decoration { + /* make sure selection color wins when a label is being selected */ + color: inherit !important; +} + +.monaco-tree .outline-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-tree .outline-element .outline-element-icon { + padding-right: 3px; +} + +/* .monaco-tree.no-icons .outline-element .outline-element-icon { + display: none; +} */ + +.monaco-tree .outline-element .outline-element-label { + text-overflow: ellipsis; + overflow: hidden; + color: var(--outline-element-color); +} + +.monaco-tree .outline-element .outline-element-label .monaco-highlighted-label .highlight { + font-weight: bold; +} + +.monaco-tree .outline-element .outline-element-detail { + visibility: hidden; + flex: 1; + flex-basis: 10%; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + font-size: 90%; + padding-left: 4px; + padding-top: 3px; +} + +.monaco-tree .monaco-tree-row.focused .outline-element .outline-element-detail { + visibility: inherit; +} + +.monaco-tree .outline-element .outline-element-decoration { + opacity: 0.75; + font-size: 90%; + font-weight: 600; + padding: 0 12px 0 5px; + margin-left: auto; + text-align: center; + color: var(--outline-element-color); +} + +.monaco-tree .outline-element .outline-element-decoration.bubble { + font-family: octicons; + font-size: 14px; + opacity: 0.4; +} diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index f448469a22f..a2a8a2447f8 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -6,8 +6,6 @@ import { DocumentSymbolProviderRegistry, DocumentSymbolProvider, DocumentSymbol } from 'vs/editor/common/modes'; import { ITextModel } from 'vs/editor/common/model'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { TPromise } from 'vs/base/common/winjs.base'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; import { IPosition } from 'vs/editor/common/core/position'; import { Range, IRange } from 'vs/editor/common/core/range'; @@ -17,6 +15,7 @@ import { commonPrefixLength } from 'vs/base/common/strings'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { LRUCache } from 'vs/base/common/map'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export abstract class TreeElement { abstract id: string; @@ -196,7 +195,7 @@ export class OutlineGroup extends TreeElement { export class OutlineModel extends TreeElement { - private static readonly _requests = new LRUCache, model: OutlineModel }>(9, .75); + private static readonly _requests = new LRUCache, model: OutlineModel }>(9, .75); private static readonly _keys = new class { private _counter = 1; @@ -221,15 +220,17 @@ export class OutlineModel extends TreeElement { }; - static create(textModel: ITextModel): TPromise { + static create(textModel: ITextModel, token: CancellationToken): Promise { let key = this._keys.for(textModel); let data = OutlineModel._requests.get(key); if (!data) { + let source = new CancellationTokenSource(); data = { promiseCnt: 0, - promise: OutlineModel._create(textModel), + source, + promise: OutlineModel._create(textModel, source.token), model: undefined, }; OutlineModel._requests.set(key, data); @@ -237,13 +238,21 @@ export class OutlineModel extends TreeElement { if (data.model) { // resolved -> return data - return TPromise.as(data.model); + return Promise.resolve(data.model); } // increase usage counter data.promiseCnt += 1; - return new TPromise((resolve, reject) => { + token.onCancellationRequested(() => { + // last -> cancel provider request, remove cached promise + if (--data.promiseCnt === 0) { + data.source.cancel(); + OutlineModel._requests.delete(key); + } + }); + + return new Promise((resolve, reject) => { data.promise.then(model => { data.model = model; resolve(model); @@ -251,16 +260,10 @@ export class OutlineModel extends TreeElement { OutlineModel._requests.delete(key); reject(err); }); - }, () => { - // last -> cancel provider request, remove cached promise - if (--data.promiseCnt === 0) { - data.promise.cancel(); - OutlineModel._requests.delete(key); - } }); } - static _create(textModel: ITextModel): TPromise { + static _create(textModel: ITextModel, token: CancellationToken): Promise { let result = new OutlineModel(textModel); let promises = DocumentSymbolProviderRegistry.ordered(textModel).map((provider, index) => { @@ -268,7 +271,7 @@ export class OutlineModel extends TreeElement { let id = TreeElement.findId(`provider_${index}`, result); let group = new OutlineGroup(id, result, provider, index); - return asWinJsPromise(token => provider.provideDocumentSymbols(result.textModel, token)).then(result => { + return Promise.resolve(provider.provideDocumentSymbols(result.textModel, token)).then(result => { if (!isFalsyOrEmpty(result)) { for (const info of result) { OutlineModel._makeOutlineElement(info, group); @@ -283,7 +286,7 @@ export class OutlineModel extends TreeElement { }); }); - return TPromise.join(promises).then(() => { + return Promise.all(promises).then(() => { let count = 0; for (const key in result._groups) { diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 19489cbae98..e1181ea33cd 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -5,22 +5,23 @@ 'use strict'; import * as dom from 'vs/base/browser/dom'; -import 'vs/css!./media/symbol-icons'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { values } from 'vs/base/common/collections'; import { createMatches } from 'vs/base/common/filters'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; +import 'vs/css!./media/outlineTree'; +import 'vs/css!./media/symbol-icons'; import { Range } from 'vs/editor/common/core/range'; -import { symbolKindToCssClass, SymbolKind } from 'vs/editor/common/modes'; +import { SymbolKind, symbolKindToCssClass } from 'vs/editor/common/modes'; import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export enum OutlineItemCompareType { ByPosition, diff --git a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts index bd34ffd3550..c07576219f8 100644 --- a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts +++ b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts @@ -12,6 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { TextModel } from 'vs/editor/common/model/textModel'; import URI from 'vs/base/common/uri'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; suite('OutlineModel', function () { @@ -26,16 +27,16 @@ suite('OutlineModel', function () { } }); - await OutlineModel.create(model); + await OutlineModel.create(model, CancellationToken.None); assert.equal(count, 1); // cached - await OutlineModel.create(model); + await OutlineModel.create(model, CancellationToken.None); assert.equal(count, 1); // new version model.applyEdits([{ text: 'XXX', range: new Range(1, 1, 1, 1) }]); - await OutlineModel.create(model); + await OutlineModel.create(model, CancellationToken.None); assert.equal(count, 2); reg.dispose(); @@ -58,13 +59,15 @@ suite('OutlineModel', function () { }); assert.equal(isCancelled, false); - let p1 = OutlineModel.create(model); - let p2 = OutlineModel.create(model); + let s1 = new CancellationTokenSource(); + OutlineModel.create(model, s1.token); + let s2 = new CancellationTokenSource(); + OutlineModel.create(model, s2.token); - p1.cancel(); + s1.cancel(); assert.equal(isCancelled, false); - p2.cancel(); + s2.cancel(); assert.equal(isCancelled, true); reg.dispose(); diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 68151b424c8..389901f50ff 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -9,7 +9,7 @@ import 'vs/css!./folding'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { RunOnceScheduler, Delayer, asWinJsPromise } from 'vs/base/common/async'; +import { RunOnceScheduler, Delayer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -67,7 +67,7 @@ export class FoldingController implements IEditorContribution { private hiddenRangeModel: HiddenRangeModel; private rangeProvider: RangeProvider; - private foldingRegionPromise: TPromise; + private foldingRegionPromise: CancelablePromise; private foldingStateMemento: FoldingStateMemento; @@ -261,8 +261,8 @@ export class FoldingController implements IEditorContribution { if (!this.foldingModel) { // null if editor has been disposed, or folding turned off return null; } - let foldingRegionPromise = this.foldingRegionPromise = asWinJsPromise(token => this.getRangeProvider(this.foldingModel.textModel).compute(token)); - return foldingRegionPromise.then(foldingRanges => { + let foldingRegionPromise = this.foldingRegionPromise = createCancelablePromise(token => this.getRangeProvider(this.foldingModel.textModel).compute(token)); + return TPromise.wrap(foldingRegionPromise.then(foldingRanges => { if (foldingRanges && foldingRegionPromise === this.foldingRegionPromise) { // new request or cancelled in the meantime? // some cursors might have moved into hidden regions, make sure they are in expanded regions let selections = this.editor.getSelections(); @@ -270,7 +270,7 @@ export class FoldingController implements IEditorContribution { this.foldingModel.update(foldingRanges, selectionLineNumbers); } return this.foldingModel; - }); + })); }); } } diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts index 849dd29d8d0..8c15d98772c 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts @@ -26,6 +26,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { createCancelablePromise } from 'vs/base/common/async'; export class DefinitionActionConfig { @@ -159,7 +160,7 @@ export class DefinitionAction extends EditorAction { private _openInPeek(editorService: ICodeEditorService, target: ICodeEditor, model: ReferencesModel) { let controller = ReferencesController.get(target); if (controller) { - controller.toggleWidget(target.getSelection(), TPromise.as(model), { + controller.toggleWidget(target.getSelection(), createCancelablePromise(_ => Promise.resolve(model)), { getMetaTitle: (model) => { return this._getMetaTitle(model); }, diff --git a/src/vs/editor/contrib/hover/getHover.ts b/src/vs/editor/contrib/hover/getHover.ts index a6bf28af9c8..466f04cab88 100644 --- a/src/vs/editor/contrib/hover/getHover.ts +++ b/src/vs/editor/contrib/hover/getHover.ts @@ -7,22 +7,19 @@ import { coalesce } from 'vs/base/common/arrays'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Hover, HoverProviderRegistry } from 'vs/editor/common/modes'; -import { asWinJsPromise } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; -export function getHover(model: ITextModel, position: Position): TPromise { +export function getHover(model: ITextModel, position: Position, token: CancellationToken = CancellationToken.None): Promise { const supports = HoverProviderRegistry.ordered(model); const values: Hover[] = []; const promises = supports.map((support, idx) => { - return asWinJsPromise((token) => { - return support.provideHover(model, position, token); - }).then((result) => { + return Promise.resolve(support.provideHover(model, position, token)).then((result) => { if (result) { let hasRange = (typeof result.range !== 'undefined'); let hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0; @@ -35,7 +32,7 @@ export function getHover(model: ITextModel, position: Position): TPromise coalesce(values)); + return Promise.all(promises).then(() => coalesce(values)); } registerDefaultLanguageCommand('_executeHoverProvider', getHover); diff --git a/src/vs/editor/contrib/hover/hoverOperation.ts b/src/vs/editor/contrib/hover/hoverOperation.ts index fa6b0e949d1..16699608cd6 100644 --- a/src/vs/editor/contrib/hover/hoverOperation.ts +++ b/src/vs/editor/contrib/hover/hoverOperation.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IHoverComputer { /** * This is called after half the hover time */ - computeAsync?: () => TPromise; + computeAsync?: (token: CancellationToken) => Promise; /** * This is called after all the hover time @@ -57,7 +57,7 @@ export class HoverOperation { private _firstWaitScheduler: RunOnceScheduler; private _secondWaitScheduler: RunOnceScheduler; private _loadingMessageScheduler: RunOnceScheduler; - private _asyncComputationPromise: TPromise; + private _asyncComputationPromise: CancelablePromise; private _asyncComputationPromiseDone: boolean; private _completeCallback: (r: Result) => void; @@ -103,10 +103,13 @@ export class HoverOperation { if (this._computer.computeAsync) { this._asyncComputationPromiseDone = false; - this._asyncComputationPromise = this._computer.computeAsync().then((asyncResult: Result) => { - this._asyncComputationPromiseDone = true; - this._withAsyncResult(asyncResult); - }, (e) => this._onError(e)); + this._asyncComputationPromise = createCancelablePromise(token => { + return this._computer.computeAsync(token).then((asyncResult: Result) => { + this._asyncComputationPromiseDone = true; + this._withAsyncResult(asyncResult); + }, (e) => this._onError(e)); + }); + } else { this._asyncComputationPromiseDone = true; } diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 9a3665d60d8..26db8c908f0 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { HoverProviderRegistry, Hover, IColor, DocumentColorProvider } from 'vs/editor/common/modes'; @@ -21,9 +20,10 @@ import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; import { Color, RGBA } from 'vs/base/common/color'; -import { IDisposable, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { CancellationToken } from 'vs/base/common/cancellation'; const $ = dom.$; class ColorHover { @@ -57,17 +57,17 @@ class ModesContentComputer implements IHoverComputer { this._result = []; } - computeAsync(): TPromise { + computeAsync(token: CancellationToken): Promise { const model = this._editor.getModel(); if (!HoverProviderRegistry.has(model)) { - return TPromise.as(null); + return Promise.resolve(null); } return getHover(model, new Position( this._range.startLineNumber, this._range.startColumn - )); + ), token); } computeSync(): HoverPart[] { @@ -167,7 +167,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private _shouldFocus: boolean; private _colorPicker: ColorPickerWidget; - private renderDisposable: IDisposable = EmptyDisposable; + private renderDisposable: IDisposable = Disposable.None; constructor( editor: ICodeEditor, @@ -205,7 +205,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { dispose(): void { this.renderDisposable.dispose(); - this.renderDisposable = EmptyDisposable; + this.renderDisposable = Disposable.None; this._hoverOperation.cancel(); super.dispose(); } @@ -274,7 +274,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []); this._isChangingDecorations = false; this.renderDisposable.dispose(); - this.renderDisposable = EmptyDisposable; + this.renderDisposable = Disposable.None; this._colorPicker = null; } @@ -339,7 +339,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { const model = new ColorPickerModel(color, [], 0); const widget = new ColorPickerWidget(fragment, model, this._editor.getConfiguration().pixelRatio, this._themeService); - getColorPresentations(editorModel, colorInfo, msg.provider).then(colorPresentations => { + getColorPresentations(editorModel, colorInfo, msg.provider, CancellationToken.None).then(colorPresentations => { model.colorPresentations = colorPresentations; const originalText = this._editor.getModel().getValueInRange(msg.range); model.guessColorPresentation(color, originalText); @@ -381,7 +381,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { blue: color.rgba.b / 255, alpha: color.rgba.a } - }, msg.provider).then((colorPresentations) => { + }, msg.provider, CancellationToken.None).then((colorPresentations) => { model.colorPresentations = colorPresentations; }); }; diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts index 4d7b492685d..3127c005bfe 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts @@ -20,6 +20,8 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; class InPlaceReplaceController implements IEditorContribution { @@ -33,11 +35,11 @@ class InPlaceReplaceController implements IEditorContribution { className: 'valueSetReplacement' }); - private editor: ICodeEditor; - private currentRequest: TPromise; - private decorationRemover: TPromise; - private decorationIds: string[]; - private editorWorkerService: IEditorWorkerService; + private readonly editor: ICodeEditor; + private readonly editorWorkerService: IEditorWorkerService; + private decorationIds: string[] = []; + private currentRequest: CancelablePromise; + private decorationRemover: CancelablePromise; constructor( editor: ICodeEditor, @@ -45,9 +47,6 @@ class InPlaceReplaceController implements IEditorContribution { ) { this.editor = editor; this.editorWorkerService = editorWorkerService; - this.currentRequest = TPromise.as(null); - this.decorationRemover = TPromise.as(null); - this.decorationIds = []; } public dispose(): void { @@ -57,10 +56,12 @@ class InPlaceReplaceController implements IEditorContribution { return InPlaceReplaceController.ID; } - public run(source: string, up: boolean): TPromise { + public run(source: string, up: boolean): Thenable { // cancel any pending request - this.currentRequest.cancel(); + if (this.currentRequest) { + this.currentRequest.cancel(); + } let selection = this.editor.getSelection(); const model = this.editor.getModel(); @@ -74,18 +75,12 @@ class InPlaceReplaceController implements IEditorContribution { const state = new EditorState(this.editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); if (!this.editorWorkerService.canNavigateValueSet(modelURI)) { - this.currentRequest = TPromise.as(null); - } else { - this.currentRequest = this.editorWorkerService.navigateValueSet(modelURI, selection, up); - this.currentRequest = this.currentRequest.then((basicResult) => { - if (basicResult && basicResult.range && basicResult.value) { - return basicResult; - } - return null; - }); + return undefined; } - return this.currentRequest.then((result: IInplaceReplaceSupportResult) => { + this.currentRequest = createCancelablePromise(token => this.editorWorkerService.navigateValueSet(modelURI, selection, up)); + + return this.currentRequest.then(result => { if (!result || !result.range || !result.value) { // No proper result @@ -127,12 +122,13 @@ class InPlaceReplaceController implements IEditorContribution { }]); // remove decoration after delay - this.decorationRemover.cancel(); - this.decorationRemover = TPromise.timeout(350); - this.decorationRemover.then(() => { - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); - }); - }); + if (this.decorationRemover) { + this.decorationRemover.cancel(); + } + this.decorationRemover = timeout(350); + this.decorationRemover.then(() => this.decorationIds = this.editor.deltaDecorations(this.decorationIds, [])).catch(onUnexpectedError); + + }).catch(onUnexpectedError); } } @@ -156,7 +152,7 @@ class InPlaceReplaceUp extends EditorAction { if (!controller) { return undefined; } - return controller.run(this.id, true); + return TPromise.wrap(controller.run(this.id, true)); } } @@ -180,7 +176,7 @@ class InPlaceReplaceDown extends EditorAction { if (!controller) { return undefined; } - return controller.run(this.id, false); + return TPromise.wrap(controller.run(this.id, false)); } } diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index bbeda1aedd4..fb5f845e883 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -14,6 +14,7 @@ import { ILink, LinkProvider, LinkProviderRegistry } from 'vs/editor/common/mode import { asWinJsPromise } from 'vs/base/common/async'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class Link implements ILink { @@ -65,13 +66,13 @@ export class Link implements ILink { } } -export function getLinks(model: ITextModel): TPromise { +export function getLinks(model: ITextModel, token: CancellationToken): Promise { let links: Link[] = []; // ask all providers for links in parallel const promises = LinkProviderRegistry.ordered(model).reverse().map(provider => { - return asWinJsPromise(token => provider.provideLinks(model, token)).then(result => { + return Promise.resolve(provider.provideLinks(model, token)).then(result => { if (Array.isArray(result)) { const newLinks = result.map(link => new Link(link, provider)); links = union(links, newLinks); @@ -79,7 +80,7 @@ export function getLinks(model: ITextModel): TPromise { }, onUnexpectedExternalError); }); - return TPromise.join(promises).then(() => { + return Promise.all(promises).then(() => { return links; }); } @@ -137,5 +138,5 @@ CommandsRegistry.registerCommand('_executeLinkProvider', (accessor, ...args) => return undefined; } - return getLinks(model); + return getLinks(model, CancellationToken.None); }); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index cea850c2a20..87fe09b073e 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -9,7 +9,6 @@ import 'vs/css!./links'; import * as nls from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as platform from 'vs/base/common/platform'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; @@ -25,6 +24,7 @@ import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'v import { MarkdownString } from 'vs/base/common/htmlContent'; import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import * as async from 'vs/base/common/async'; const HOVER_MESSAGE_GENERAL_META = new MarkdownString().appendText( platform.isMacintosh @@ -149,8 +149,8 @@ class LinkDetector implements editorCommon.IEditorContribution { private editor: ICodeEditor; private enabled: boolean; private listenersToRemove: IDisposable[]; - private timeoutPromise: TPromise; - private computePromise: TPromise; + private timeoutPromise: async.CancelablePromise; + private computePromise: async.CancelablePromise; private activeLinkDecorationId: string; private openerService: IOpenerService; private notificationService: INotificationService; @@ -226,7 +226,7 @@ class LinkDetector implements editorCommon.IEditorContribution { private onChange(): void { if (!this.timeoutPromise) { - this.timeoutPromise = TPromise.timeout(LinkDetector.RECOMPUTE_TIME); + this.timeoutPromise = async.timeout(LinkDetector.RECOMPUTE_TIME); this.timeoutPromise.then(() => { this.timeoutPromise = null; this.beginCompute(); @@ -234,7 +234,7 @@ class LinkDetector implements editorCommon.IEditorContribution { } } - private beginCompute(): void { + private async beginCompute(): Promise { if (!this.editor.getModel() || !this.enabled) { return; } @@ -243,10 +243,15 @@ class LinkDetector implements editorCommon.IEditorContribution { return; } - this.computePromise = getLinks(this.editor.getModel()).then(links => { + this.computePromise = async.createCancelablePromise(token => getLinks(this.editor.getModel(), token)); + try { + const links = await this.computePromise; this.updateDecorations(links); + } catch (err) { + onUnexpectedError(err); + } finally { this.computePromise = null; - }); + } } private updateDecorations(links: Link[]): void { diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index c0390869a9f..3d695a84c97 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -13,7 +13,7 @@ import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { SignatureHelp, SignatureInformation, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event, Emitter, chain } from 'vs/base/common/event'; import { domEvent, stop } from 'vs/base/browser/event'; @@ -50,7 +50,7 @@ export class ParameterHintsModel extends Disposable { private triggerCharactersListeners: IDisposable[]; private active: boolean; private throttledDelayer: RunOnceScheduler; - private provideSignatureHelpRequest?: TPromise; + private provideSignatureHelpRequest?: CancelablePromise; constructor(editor: ICodeEditor) { super(); @@ -103,21 +103,21 @@ export class ParameterHintsModel extends Disposable { this.provideSignatureHelpRequest.cancel(); } - this.provideSignatureHelpRequest = provideSignatureHelp(this.editor.getModel(), this.editor.getPosition()) - .then(null, onUnexpectedError) - .then(result => { - if (!result || !result.signatures || result.signatures.length === 0) { - this.cancel(); - this._onCancel.fire(void 0); - return false; - } + this.provideSignatureHelpRequest = createCancelablePromise(token => provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), token)); - this.active = true; + this.provideSignatureHelpRequest.then(result => { + if (!result || !result.signatures || result.signatures.length === 0) { + this.cancel(); + this._onCancel.fire(void 0); + return false; + } - const event: IHintEvent = { hints: result }; - this._onHint.fire(event); - return true; - }); + this.active = true; + const event: IHintEvent = { hints: result }; + this._onHint.fire(event); + return true; + + }).catch(onUnexpectedError); } isTriggered(): boolean { diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index f12e2c2eccb..6a6f30d27d5 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -3,27 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asWinJsPromise, first } from 'vs/base/common/async'; +import { first2 } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; import { SignatureHelp, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const Context = { Visible: new RawContextKey('parameterHintsVisible', false), MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp(model: ITextModel, position: Position): TPromise { +export function provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken = CancellationToken.None): Promise { const supports = SignatureHelpProviderRegistry.ordered(model); - return first(supports.map(support => () => { - return asWinJsPromise(token => support.provideSignatureHelp(model, position, token)) - .then(undefined, onUnexpectedExternalError); + return first2(supports.map(support => () => { + return Promise.resolve(support.provideSignatureHelp(model, position, token)).catch(onUnexpectedExternalError); })); } diff --git a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts index 1f68bd41f23..cb21013f62b 100644 --- a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts @@ -17,7 +17,7 @@ import { Range } from 'vs/editor/common/core/range'; import { PeekContext, getOuterEditor } from './peekViewWidget'; import { ReferencesController, RequestOptions, ctxReferenceSearchVisible } from './referencesController'; import { ReferencesModel, OneReference } from './referencesModel'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { asWinJsPromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -28,6 +28,7 @@ import { ctxReferenceWidgetSearchTreeFocused } from 'vs/editor/contrib/reference import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import URI from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const defaultReferenceSearchOptions: RequestOptions = { getMetaTitle(model) { @@ -85,7 +86,7 @@ export class ReferenceAction extends EditorAction { } let range = editor.getSelection(); let model = editor.getModel(); - let references = provideReferences(model, range.getStartPosition()).then(references => new ReferencesModel(references)); + let references = createCancelablePromise(token => provideReferences(model, range.getStartPosition(), token).then(references => new ReferencesModel(references))); controller.toggleWidget(range, references, defaultReferenceSearchOptions); } } @@ -113,7 +114,7 @@ let findReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resour return undefined; } - let references = provideReferences(control.getModel(), Position.lift(position)).then(references => new ReferencesModel(references)); + let references = createCancelablePromise(token => provideReferences(control.getModel(), Position.lift(position), token).then(references => new ReferencesModel(references))); let range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); return TPromise.as(controller.toggleWidget(range, references, defaultReferenceSearchOptions)); }); @@ -137,7 +138,7 @@ let showReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resour return TPromise.as(controller.toggleWidget( new Range(position.lineNumber, position.column, position.lineNumber, position.column), - TPromise.as(new ReferencesModel(references)), + createCancelablePromise(_ => Promise.reject(new ReferencesModel(references))), defaultReferenceSearchOptions)).then(() => true); }); }; @@ -266,7 +267,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: openReferenceToSide }); -export function provideReferences(model: ITextModel, position: Position): TPromise { +export function provideReferences(model: ITextModel, position: Position, token: CancellationToken): Promise { // collect references from all providers const promises = ReferenceProviderRegistry.ordered(model).map(provider => { @@ -282,7 +283,7 @@ export function provideReferences(model: ITextModel, position: Position): TPromi }); }); - return TPromise.join(promises).then(references => { + return Promise.all(promises).then(references => { let result: Location[] = []; for (let ref of references) { if (ref) { diff --git a/src/vs/editor/contrib/referenceSearch/referencesController.ts b/src/vs/editor/contrib/referenceSearch/referencesController.ts index 2c03e0d6ebc..92dcaddafe9 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesController.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesController.ts @@ -25,6 +25,7 @@ import { Position } from 'vs/editor/common/core/position'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Location } from 'vs/editor/common/modes'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CancelablePromise } from 'vs/base/common/async'; export const ctxReferenceSearchVisible = new RawContextKey('referenceSearchVisible', false); @@ -82,7 +83,7 @@ export abstract class ReferencesController implements editorCommon.IEditorContri this._editor = null; } - public toggleWidget(range: Range, modelPromise: TPromise, options: RequestOptions): void { + public toggleWidget(range: Range, modelPromise: CancelablePromise, options: RequestOptions): void { // close current widget and return early is position didn't change let widgetPosition: Position; diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 83abd441a26..3022a612e99 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -205,6 +205,10 @@ export class SnippetController2 implements IEditorContribution { this._updateState(); } + isInSnippet(): boolean { + return this._inSnippet.get(); + } + getSessionEnclosingRange(): Range { if (this._session) { return this._session.getEnclosingRange(); diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index d67493343bf..06208d010d0 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -9,7 +9,7 @@ import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore } from 'vs/base/comm import { isDisposable } from 'vs/base/common/lifecycle'; import { ISuggestResult, ISuggestSupport } from 'vs/editor/common/modes'; import { ISuggestionItem } from './suggest'; -import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; +import { InternalSuggestOptions, EDITOR_DEFAULTS } from 'vs/editor/common/config/editorOptions'; export interface ICompletionItem extends ISuggestionItem { matches?: number[]; @@ -58,7 +58,7 @@ export class CompletionModel { private _isIncomplete: Set; private _stats: ICompletionStats; - constructor(items: ISuggestionItem[], column: number, lineContext: LineContext, options: InternalSuggestOptions = { filterGraceful: true, snippets: 'inline' }) { + constructor(items: ISuggestionItem[], column: number, lineContext: LineContext, options: InternalSuggestOptions = EDITOR_DEFAULTS.contribInfo.suggest) { this._items = items; this._column = column; this._options = options; diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 566e1b12963..9bfe478497b 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asWinJsPromise, first } from 'vs/base/common/async'; +import { first2 } from 'vs/base/common/async'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { compareIgnoreCase } from 'vs/base/common/strings'; import { assign } from 'vs/base/common/objects'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; @@ -16,6 +15,7 @@ import { ISuggestResult, ISuggestSupport, ISuggestion, SuggestRegistry, SuggestC import { Position, IPosition } from 'vs/editor/common/core/position'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const Context = { Visible: new RawContextKey('suggestWidgetVisible', false), @@ -30,7 +30,7 @@ export interface ISuggestionItem { suggestion: ISuggestion; container: ISuggestResult; support: ISuggestSupport; - resolve(): TPromise; + resolve(token: CancellationToken): Thenable; } export type SnippetConfig = 'top' | 'bottom' | 'inline' | 'none'; @@ -47,7 +47,14 @@ export function setSnippetSuggestSupport(support: ISuggestSupport): ISuggestSupp return old; } -export function provideSuggestionItems(model: ITextModel, position: Position, snippetConfig: SnippetConfig = 'bottom', onlyFrom?: ISuggestSupport[], context?: SuggestContext): TPromise { +export function provideSuggestionItems( + model: ITextModel, + position: Position, + snippetConfig: SnippetConfig = 'bottom', + onlyFrom?: ISuggestSupport[], + context?: SuggestContext, + token: CancellationToken = CancellationToken.None +): Promise { const allSuggestions: ISuggestionItem[] = []; const acceptSuggestion = createSuggesionFilter(snippetConfig); @@ -69,13 +76,13 @@ export function provideSuggestionItems(model: ITextModel, position: Position, sn let hasResult = false; const factory = supports.map(supports => () => { // for each support in the group ask for suggestions - return TPromise.join(supports.map(support => { + return Promise.all(supports.map(support => { if (!isFalsyOrEmpty(onlyFrom) && onlyFrom.indexOf(support) < 0) { return undefined; } - return asWinJsPromise(token => support.provideCompletionItems(model, position, suggestConext, token)).then(container => { + return Promise.resolve(support.provideCompletionItems(model, position, suggestConext, token)).then(container => { const len = allSuggestions.length; @@ -104,7 +111,7 @@ export function provideSuggestionItems(model: ITextModel, position: Position, sn })); }); - const result = first(factory, () => hasResult).then(() => allSuggestions.sort(getSuggestionComparator(snippetConfig))); + const result = first2(factory, () => hasResult).then(() => allSuggestions.sort(getSuggestionComparator(snippetConfig))); // result.then(items => { // console.log(model.getWordUntilPosition(position), items.map(item => `${item.suggestion.label}, type=${item.suggestion.type}, incomplete?${item.container.incomplete}, overwriteBefore=${item.suggestion.overwriteBefore}`)); @@ -125,13 +132,13 @@ function fixOverwriteBeforeAfter(suggestion: ISuggestion, container: ISuggestRes } } -function createSuggestionResolver(provider: ISuggestSupport, suggestion: ISuggestion, model: ITextModel, position: Position): () => TPromise { - return () => { +function createSuggestionResolver(provider: ISuggestSupport, suggestion: ISuggestion, model: ITextModel, position: Position): (token: CancellationToken) => Promise { + return (token) => { if (typeof provider.resolveCompletionItem === 'function') { - return asWinJsPromise(token => provider.resolveCompletionItem(model, position, suggestion, token)) - .then(value => { assign(suggestion, value); }); + return Promise.resolve(provider.resolveCompletionItem(model, position, suggestion, token)).then(value => { assign(suggestion, value); }); + } else { + return Promise.resolve(void 0); } - return TPromise.as(void 0); }; } @@ -213,13 +220,13 @@ registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, positio return provideSuggestionItems(model, position).then(items => { for (const item of items) { if (resolving.length < maxItemsToResolve) { - resolving.push(item.resolve()); + resolving.push(item.resolve(CancellationToken.None)); } result.incomplete = result.incomplete || item.container.incomplete; result.suggestions.push(item.suggestion); } }).then(() => { - return TPromise.join(resolving); + return Promise.all(resolving); }).then(() => { return result; }); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index d95f90c5e8a..4a427b85dfe 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -5,12 +5,11 @@ 'use strict'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { TimeoutTimer } from 'vs/base/common/async'; +import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; @@ -19,6 +18,7 @@ import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; import { ISuggestSupport, StandardTokenType, SuggestContext, SuggestRegistry, SuggestTriggerKind } from 'vs/editor/common/modes'; import { CompletionModel } from './completionModel'; import { ISuggestionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport } from './suggest'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; export interface ICancelEvent { readonly retrigger: boolean; @@ -89,11 +89,11 @@ export class SuggestModel implements IDisposable { private _toDispose: IDisposable[] = []; private _quickSuggestDelay: number; private _triggerCharacterListener: IDisposable; - private _triggerAutoSuggestPromise: TPromise; - private _triggerRefilter = new TimeoutTimer(); + private readonly _triggerQuickSuggest = new TimeoutTimer(); + private readonly _triggerRefilter = new TimeoutTimer(); private _state: State; - private _requestPromise: TPromise; + private _requestPromise: CancelablePromise; private _context: LineContext; private _currentSelection: Selection; @@ -109,7 +109,6 @@ export class SuggestModel implements IDisposable { constructor(editor: ICodeEditor) { this._editor = editor; this._state = State.Idle; - this._triggerAutoSuggestPromise = null; this._requestPromise = null; this._completionModel = null; this._context = null; @@ -144,7 +143,7 @@ export class SuggestModel implements IDisposable { } dispose(): void { - dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this._triggerCharacterListener, this._triggerRefilter]); + dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this._triggerCharacterListener, this._triggerQuickSuggest, this._triggerRefilter]); this._toDispose = dispose(this._toDispose); dispose(this._completionModel); this.cancel(); @@ -209,9 +208,9 @@ export class SuggestModel implements IDisposable { this._triggerRefilter.cancel(); - if (this._triggerAutoSuggestPromise) { - this._triggerAutoSuggestPromise.cancel(); - this._triggerAutoSuggestPromise = null; + if (this._triggerQuickSuggest) { + this._triggerQuickSuggest.cancel(); + } if (this._requestPromise) { @@ -265,48 +264,58 @@ export class SuggestModel implements IDisposable { if (this._state === State.Idle) { - // trigger 24x7 IntelliSense when idle, enabled, when cursor - // moved RIGHT, and when at a good position - if (this._editor.getConfiguration().contribInfo.quickSuggestions !== false - && (prevSelection.containsRange(this._currentSelection) - || prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) - ) { - this.cancel(); - - this._triggerAutoSuggestPromise = TPromise.timeout(this._quickSuggestDelay); - this._triggerAutoSuggestPromise.then(() => { - if (LineContext.shouldAutoTrigger(this._editor)) { - const model = this._editor.getModel(); - const pos = this._editor.getPosition(); - - if (!model) { - return; - } - // validate enabled now - const { quickSuggestions } = this._editor.getConfiguration().contribInfo; - if (quickSuggestions === false) { - return; - } else if (quickSuggestions === true) { - // all good - } else { - // Check the type of the token that triggered this - model.tokenizeIfCheap(pos.lineNumber); - const lineTokens = model.getLineTokens(pos.lineNumber); - const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); - const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other - || quickSuggestions.comments && tokenType === StandardTokenType.Comment - || quickSuggestions.strings && tokenType === StandardTokenType.String; - - if (!inValidScope) { - return; - } - } - - this.trigger({ auto: true }); - } - this._triggerAutoSuggestPromise = null; - }); + if (this._editor.getConfiguration().contribInfo.quickSuggestions === false) { + // not enabled + return; } + + if (!prevSelection.containsRange(this._currentSelection) && !prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) { + // cursor didn't move RIGHT + return; + } + + if (this._editor.getConfiguration().contribInfo.suggest.snippetsPreventQuickSuggestions && SnippetController2.get(this._editor).isInSnippet()) { + // no quick suggestion when in snippet mode + return; + } + + this.cancel(); + + this._triggerQuickSuggest.cancelAndSet(() => { + if (!LineContext.shouldAutoTrigger(this._editor)) { + return; + } + + const model = this._editor.getModel(); + const pos = this._editor.getPosition(); + if (!model) { + return; + } + // validate enabled now + const { quickSuggestions } = this._editor.getConfiguration().contribInfo; + if (quickSuggestions === false) { + return; + } else if (quickSuggestions === true) { + // all good + } else { + // Check the type of the token that triggered this + model.tokenizeIfCheap(pos.lineNumber); + const lineTokens = model.getLineTokens(pos.lineNumber); + const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); + const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other + || quickSuggestions.comments && tokenType === StandardTokenType.Comment + || quickSuggestions.strings && tokenType === StandardTokenType.String; + + if (!inValidScope) { + return; + } + } + + // we made it till here -> trigger now + this.trigger({ auto: true }); + + }, this._quickSuggestDelay); + } } @@ -356,11 +365,16 @@ export class SuggestModel implements IDisposable { suggestCtx = { triggerKind: SuggestTriggerKind.Invoke }; } - this._requestPromise = provideSuggestionItems(model, this._editor.getPosition(), + this._requestPromise = createCancelablePromise(token => provideSuggestionItems( + model, + this._editor.getPosition(), this._editor.getConfiguration().contribInfo.suggest.snippets, onlyFrom, - suggestCtx - ).then(items => { + suggestCtx, + token + )); + + this._requestPromise.then(items => { this._requestPromise = null; if (this._state === State.Idle) { @@ -386,7 +400,7 @@ export class SuggestModel implements IDisposable { ); this._onNewContext(ctx); - }).then(null, onUnexpectedError); + }).catch(onUnexpectedError); } private _onNewContext(ctx: LineContext): void { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 4d45c78a181..6e72075c97e 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -10,8 +10,7 @@ import * as nls from 'vs/nls'; import { createMatches } from 'vs/base/common/filters'; import * as strings from 'vs/base/common/strings'; import { Event, Emitter, chain } from 'vs/base/common/event'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; @@ -33,6 +32,8 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; const sticky = false; // for development purposes const expandSuggestionDocsByDefault = false; @@ -361,7 +362,7 @@ export class SuggestWidget implements IContentWidget, IDelegate private state: State; private isAuto: boolean; private loadingTimeout: number; - private currentSuggestionDetails: TPromise; + private currentSuggestionDetails: CancelablePromise; private focusedItem: ICompletionItem; private ignoreFocusEvents = false; private completionModel: CompletionModel; @@ -377,8 +378,8 @@ export class SuggestWidget implements IContentWidget, IDelegate private suggestWidgetMultipleSuggestions: IContextKey; private suggestionSupportsAutoAccept: IContextKey; - private editorBlurTimeout: TPromise; - private showTimeout: TPromise; + private readonly editorBlurTimeout = new TimeoutTimer(); + private readonly showTimeout = new TimeoutTimer(); private toDispose: IDisposable[]; private onDidSelectEmitter = new Emitter(); @@ -482,11 +483,11 @@ export class SuggestWidget implements IContentWidget, IDelegate return; } - this.editorBlurTimeout = TPromise.timeout(150).then(() => { + this.editorBlurTimeout.cancelAndSet(() => { if (!this.editor.hasTextFocus()) { this.setState(State.Hidden); } - }); + }, 150); } private onEditorLayoutChange(): void { @@ -502,7 +503,7 @@ export class SuggestWidget implements IContentWidget, IDelegate const item = e.elements[0]; const index = e.indexes[0]; - item.resolve().then(() => { + item.resolve(CancellationToken.None).then(() => { this.onDidSelectEmitter.fire({ item, index, model: this.completionModel }); alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.suggestion.label)); this.editor.focus(); @@ -586,22 +587,21 @@ export class SuggestWidget implements IContentWidget, IDelegate this.list.reveal(index); - this.currentSuggestionDetails = item.resolve() - .then(() => { - // item can have extra information, so re-render - this.ignoreFocusEvents = true; - this.list.splice(index, 1, [item]); - this.list.setFocus([index]); - this.ignoreFocusEvents = false; + this.currentSuggestionDetails = createCancelablePromise(token => item.resolve(token)); - if (this.expandDocsSettingFromStorage()) { - this.showDetails(); - } else { - removeClass(this.element, 'docs-side'); - } - }) - .then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err)) - .then(() => this.currentSuggestionDetails = null); + this.currentSuggestionDetails.then(() => { + // item can have extra information, so re-render + this.ignoreFocusEvents = true; + this.list.splice(index, 1, [item]); + this.list.setFocus([index]); + this.ignoreFocusEvents = false; + + if (this.expandDocsSettingFromStorage()) { + this.showDetails(); + } else { + removeClass(this.element, 'docs-side'); + } + }).catch(onUnexpectedError).then(() => this.currentSuggestionDetails = null); // emit an event this.onDidFocusEmitter.fire({ item, index, model: this.completionModel }); @@ -925,10 +925,10 @@ export class SuggestWidget implements IContentWidget, IDelegate this.suggestWidgetVisible.set(true); - this.showTimeout = TPromise.timeout(100).then(() => { + this.showTimeout.cancelAndSet(() => { addClass(this.element, 'visible'); this.onDidShowEmitter.fire(this); - }); + }, 100); } private hide(): void { @@ -1082,15 +1082,8 @@ export class SuggestWidget implements IContentWidget, IDelegate this.loadingTimeout = null; } - if (this.editorBlurTimeout) { - this.editorBlurTimeout.cancel(); - this.editorBlurTimeout = null; - } - - if (this.showTimeout) { - this.showTimeout.cancel(); - this.showTimeout = null; - } + this.editorBlurTimeout.dispose(); + this.showTimeout.dispose(); } } diff --git a/src/vs/editor/contrib/suggest/test/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/completionModel.test.ts index 2786aef089a..bcb1a675914 100644 --- a/src/vs/editor/contrib/suggest/test/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/completionModel.test.ts @@ -167,7 +167,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: 's', characterCountDelta: 0 - }, { snippets: 'top', filterGraceful: true }); + }, { snippets: 'top', snippetsPreventQuickSuggestions: true, filterGraceful: true }); assert.equal(model.items.length, 2); const [a, b] = model.items; @@ -186,7 +186,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: 's', characterCountDelta: 0 - }, { snippets: 'bottom', filterGraceful: true }); + }, { snippets: 'bottom', snippetsPreventQuickSuggestions: true, filterGraceful: true }); assert.equal(model.items.length, 2); const [a, b] = model.items; @@ -204,7 +204,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: 's', characterCountDelta: 0 - }, { snippets: 'inline', filterGraceful: true }); + }, { snippets: 'inline', snippetsPreventQuickSuggestions: true, filterGraceful: true }); assert.equal(model.items.length, 2); const [a, b] = model.items; diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 69a6c6870e0..d5a5daeae4b 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -30,13 +30,15 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; function createMockEditor(model: TextModel): TestCodeEditor { - return createTestCodeEditor({ + let editor = createTestCodeEditor({ model: model, serviceCollection: new ServiceCollection( [ITelemetryService, NullTelemetryService], [IStorageService, NullStorageService] - ) + ), }); + editor.registerAndInstantiateContribution(SnippetController2); + return editor; } suite('SuggestModel - Context', function () { diff --git a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts index b54ddc7f25a..1b7b9016979 100644 --- a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts @@ -5,9 +5,8 @@ import * as nls from 'vs/nls'; -import { asWinJsPromise, first } from 'vs/base/common/async'; +import { first2, createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorContribution, EditorAction, IActionOptions, registerEditorAction, registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; @@ -25,6 +24,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { firstIndex, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, TrackedRangeStickiness, OverviewRulerLane, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const editorWordHighlight = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hc: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable. The color must not be opaque to not hide underlying decorations.'), true); export const editorWordHighlightStrong = registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hc: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable. The color must not be opaque to not hide underlying decorations.'), true); @@ -36,15 +36,15 @@ export const overviewRulerWordHighlightStrongForeground = registerColor('editorO export const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); -export function getOccurrencesAtPosition(model: ITextModel, position: Position): TPromise { +export function getOccurrencesAtPosition(model: ITextModel, position: Position, token: CancellationToken = CancellationToken.None): Promise { const orderedByScore = DocumentHighlightProviderRegistry.ordered(model); // in order of score ask the occurrences provider // until someone response with a good result // (good = none empty array) - return first(orderedByScore.map(provider => () => { - return asWinJsPromise(token => provider.provideDocumentHighlights(model, position, token)) + return first2(orderedByScore.map(provider => () => { + return Promise.resolve(provider.provideDocumentHighlights(model, position, token)) .then(undefined, onUnexpectedExternalError); }), result => !isFalsyOrEmpty(result)); } @@ -61,7 +61,7 @@ class WordHighlighter { private toUnhook: IDisposable[]; private workerRequestTokenId: number = 0; - private workerRequest: TPromise = null; + private workerRequest: CancelablePromise = null; private workerRequestCompleted: boolean = false; private workerRequestValue: DocumentHighlight[] = []; @@ -291,7 +291,7 @@ class WordHighlighter { let myRequestId = ++this.workerRequestTokenId; this.workerRequestCompleted = false; - this.workerRequest = getOccurrencesAtPosition(this.model, this.editor.getPosition()); + this.workerRequest = createCancelablePromise(token => getOccurrencesAtPosition(this.model, this.editor.getPosition(), token)); this.workerRequest.then(data => { if (myRequestId === this.workerRequestTokenId) { @@ -299,7 +299,7 @@ class WordHighlighter { this.workerRequestValue = data || []; this._beginRenderDecorations(); } - }).done(); + }); } this._lastWordRange = currentWordRange; diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 298a4d7a0d5..1f5595800c8 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -42,7 +42,7 @@ export class Colorizer { let render = (str: string) => { domNode.innerHTML = str; }; - return this.colorize(modeService, text, mimeType, options).then(render, (err) => console.error(err), render); + return this.colorize(modeService, text, mimeType, options).then(render, (err) => console.error(err)); } private static _tokenizationSupportChangedPromise(language: string): TPromise { @@ -54,7 +54,7 @@ export class Colorizer { } }; - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { listener = TokenizationRegistry.onDidChange((e) => { if (e.changedLanguages.indexOf(language) >= 0) { stopListening(); diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 22b23704de2..3a1fabd9093 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -5,7 +5,7 @@ 'use strict'; -import { empty as emptyDisposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -206,7 +206,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon } if (!this._standaloneKeybindingService) { console.warn('Cannot add keybinding because the editor is configured with an unrecognized KeybindingService'); - return emptyDisposable; + return Disposable.None; } // Read descriptor options diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index cb68bdb514e..d176c102959 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -83,7 +83,6 @@ declare namespace monaco { public static join(promises: [T1 | PromiseLike, T2 | PromiseLike]): Promise<[T1, T2]>; public static join(promises: (T | PromiseLike)[]): Promise; - public static join(promises: { [n: string]: T | PromiseLike }): Promise<{ [n: string]: T }>; public static any(promises: (T | PromiseLike)[]): Promise<{ key: string; value: Promise; }>; @@ -2514,6 +2513,10 @@ declare namespace monaco.editor { * Enable graceful matching. Defaults to true. */ filterGraceful?: boolean; + /** + * Prevent quick suggestions when a snippet is active. Defaults to true. + */ + snippetsPreventQuickSuggestions?: boolean; } /** @@ -2736,7 +2739,7 @@ declare namespace monaco.editor { /** * Configure the editor's hover. */ - hover?: boolean | IEditorHoverOptions; + hover?: IEditorHoverOptions; /** * Enable detecting links and making them clickable. * Defaults to true. @@ -3124,6 +3127,7 @@ declare namespace monaco.editor { export interface InternalSuggestOptions { readonly filterGraceful: boolean; readonly snippets: 'top' | 'bottom' | 'inline' | 'none'; + readonly snippetsPreventQuickSuggestions: boolean; } export interface EditorWrappingInfo { diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 716575efa05..ef37ffe80d5 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -34,6 +34,12 @@ export interface IConfigurationRegistry { */ notifyConfigurationSchemaUpdated(configuration: IConfigurationNode): void; + /** + * Event that fires whenver a configuration has been + * registered. + */ + onDidSchemaChange: Event; + /** * Event that fires whenver a configuration has been * registered. @@ -110,6 +116,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { private overrideIdentifiers: string[] = []; private overridePropertyPattern: string; + private readonly _onDidSchemaChange: Emitter = new Emitter(); + readonly onDidSchemaChange: Event = this._onDidSchemaChange.event; + private readonly _onDidRegisterConfiguration: Emitter = new Emitter(); readonly onDidRegisterConfiguration: Event = this._onDidRegisterConfiguration.event; @@ -176,7 +185,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, scope: ConfigurationScope = ConfigurationScope.WINDOW, overridable: boolean = false): string[] { - scope = configuration.scope !== void 0 && configuration.scope !== null ? configuration.scope : scope; + scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope; overridable = configuration.overridable || overridable; let propertyKeys = []; let properties = configuration.properties; @@ -198,8 +207,11 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (overridable) { property.overridable = true; } - if (property.scope === void 0) { - property.scope = scope; + + if (OVERRIDE_PROPERTY_PATTERN.test(key)) { + property.scope = void 0; // No scope for overridable properties `[${identifier}]` + } else { + property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope; } // Add to properties maps @@ -240,7 +252,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { function register(configuration: IConfigurationNode) { let properties = configuration.properties; if (properties) { - for (let key in properties) { + for (const key in properties) { allSettings.properties[key] = properties[key]; switch (properties[key].scope) { case ConfigurationScope.APPLICATION: @@ -261,6 +273,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } } register(configuration); + this._onDidSchemaChange.fire(); } private updateSchemaForOverrideSettingsConfiguration(configuration: IConfigurationNode): void { @@ -292,6 +305,8 @@ class ConfigurationRegistry implements IConfigurationRegistry { applicationSettings.patternProperties[this.overridePropertyPattern] = patternProperties; windowSettings.patternProperties[this.overridePropertyPattern] = patternProperties; resourceSettings.patternProperties[this.overridePropertyPattern] = patternProperties; + + this._onDidSchemaChange.fire(); } private update(configuration: IConfigurationNode): void { diff --git a/src/vs/platform/dialogs/common/dialogIpc.ts b/src/vs/platform/dialogs/common/dialogIpc.ts index 7d260b4e8f8..0a73419d531 100644 --- a/src/vs/platform/dialogs/common/dialogIpc.ts +++ b/src/vs/platform/dialogs/common/dialogIpc.ts @@ -9,6 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; +import { Event } from 'vs/base/common/event'; export interface IDialogChannel extends IChannel { call(command: 'show'): TPromise; @@ -18,7 +19,10 @@ export interface IDialogChannel extends IChannel { export class DialogChannel implements IDialogChannel { - constructor(@IDialogService private dialogService: IDialogService) { + constructor(@IDialogService private dialogService: IDialogService) { } + + listen(event: string): Event { + throw new Error('No event found'); } call(command: string, args?: any[]): TPromise { diff --git a/src/vs/platform/dialogs/node/dialogService.ts b/src/vs/platform/dialogs/node/dialogService.ts index b1896b82a8f..4304af55184 100644 --- a/src/vs/platform/dialogs/node/dialogService.ts +++ b/src/vs/platform/dialogs/node/dialogService.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; +import { canceled } from 'vs/base/common/errors'; export class CommandLineDialogService implements IDialogService { @@ -31,7 +32,7 @@ export class CommandLineDialogService implements IDialogService { }); rl.once('SIGINT', () => { rl.close(); - promise.cancel(); + e(canceled()); }); }); return promise; @@ -64,4 +65,4 @@ export class CommandLineDialogService implements IDialogService { } as IConfirmationResult; }); } -} \ No newline at end of file +} diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index a069d1c087d..82e907f2e25 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; export const ID = 'driverService'; export const IDriver = createDecorator(ID); @@ -65,6 +66,10 @@ export class DriverChannel implements IDriverChannel { constructor(private driver: IDriver) { } + listen(event: string): Event { + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'getWindowIds': return this.driver.getWindowIds(); @@ -164,6 +169,10 @@ export class WindowDriverRegistryChannel implements IWindowDriverRegistryChannel constructor(private registry: IWindowDriverRegistry) { } + listen(event: string): Event { + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'registerWindowDriver': return this.registry.registerWindowDriver(arg); @@ -218,6 +227,10 @@ export class WindowDriverChannel implements IWindowDriverChannel { constructor(private driver: IWindowDriver) { } + listen(event: string): Event { + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'click': return this.driver.click(arg[0], arg[1], arg[2]); diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts index 6c723a99181..4dbaaff6c95 100644 --- a/src/vs/platform/driver/electron-browser/driver.ts +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -50,7 +50,7 @@ class WindowDriver implements IWindowDriver { @IWindowService private windowService: IWindowService ) { } - async click(selector: string, xoffset?: number, yoffset?: number): TPromise { + click(selector: string, xoffset?: number, yoffset?: number): TPromise { return this._click(selector, 1, xoffset, yoffset); } @@ -58,11 +58,11 @@ class WindowDriver implements IWindowDriver { return this._click(selector, 2); } - private async _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> { + private _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> { const element = document.querySelector(selector); if (!element) { - throw new Error('Element not found'); + return TPromise.wrapError(new Error('Element not found')); } const { left, top } = getTopLeftOffset(element as HTMLElement); @@ -80,25 +80,27 @@ class WindowDriver implements IWindowDriver { x = Math.round(x); y = Math.round(y); - return { x, y }; + return TPromise.as({ x, y }); } - private async _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise { - const { x, y } = await this._getElementXY(selector, xoffset, yoffset); - const webContents = electron.remote.getCurrentWebContents(); + private _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise { + return this._getElementXY(selector, xoffset, yoffset).then(({ x, y }) => { - webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); - await TPromise.timeout(10); - webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); + const webContents = electron.remote.getCurrentWebContents(); + webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); - await TPromise.timeout(100); + return TPromise.timeout(10).then(() => { + webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); + return TPromise.timeout(100); + }); + }); } - async setValue(selector: string, text: string): TPromise { + setValue(selector: string, text: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Element not found'); + return TPromise.wrapError(new Error('Element not found')); } const inputElement = element as HTMLInputElement; @@ -106,13 +108,15 @@ class WindowDriver implements IWindowDriver { const event = new Event('input', { bubbles: true, cancelable: true }); inputElement.dispatchEvent(event); + + return TPromise.as(null); } - async getTitle(): TPromise { - return document.title; + getTitle(): TPromise { + return TPromise.as(document.title); } - async isActiveElement(selector: string): TPromise { + isActiveElement(selector: string): TPromise { const element = document.querySelector(selector); if (element !== document.activeElement) { @@ -128,13 +132,13 @@ class WindowDriver implements IWindowDriver { el = el.parentElement; } - throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'`); + return TPromise.wrapError(new Error(`Active element not found. Current active element is '${chain.join(' > ')}'`)); } - return true; + return TPromise.as(true); } - async getElements(selector: string, recursive: boolean): TPromise { + getElements(selector: string, recursive: boolean): TPromise { const query = document.querySelectorAll(selector); const result: IElement[] = []; @@ -143,14 +147,14 @@ class WindowDriver implements IWindowDriver { result.push(serializeElement(element, recursive)); } - return result; + return TPromise.as(result); } - async typeInEditor(selector: string, text: string): TPromise { + typeInEditor(selector: string, text: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Editor not found: ' + selector); + return TPromise.wrapError(new Error('Editor not found: ' + selector)); } const textarea = element as HTMLTextAreaElement; @@ -164,19 +168,21 @@ class WindowDriver implements IWindowDriver { const event = new Event('input', { 'bubbles': true, 'cancelable': true }); textarea.dispatchEvent(event); + + return TPromise.as(null); } - async getTerminalBuffer(selector: string): TPromise { + getTerminalBuffer(selector: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Terminal not found: ' + selector); + return TPromise.wrapError(new Error('Terminal not found: ' + selector)); } const xterm: Terminal = (element as any).xterm; if (!xterm) { - throw new Error('Xterm not found: ' + selector); + return TPromise.wrapError(new Error('Xterm not found: ' + selector)); } const lines: string[] = []; @@ -185,27 +191,29 @@ class WindowDriver implements IWindowDriver { lines.push(xterm._core.buffer.translateBufferLineToString(i, true)); } - return lines; + return TPromise.as(lines); } - async writeInTerminal(selector: string, text: string): TPromise { + writeInTerminal(selector: string, text: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Element not found'); + return TPromise.wrapError(new Error('Element not found')); } const xterm: Terminal = (element as any).xterm; if (!xterm) { - throw new Error('Xterm not found'); + return TPromise.wrapError(new Error('Xterm not found')); } xterm._core.send(text); + + return TPromise.as(null); } - async openDevTools(): TPromise { - await this.windowService.openDevTools({ mode: 'detach' }); + openDevTools(): TPromise { + return this.windowService.openDevTools({ mode: 'detach' }); } } @@ -213,7 +221,7 @@ export async function registerWindowDriver( client: IPCClient, windowId: number, instantiationService: IInstantiationService -): TPromise { +): Promise { const windowDriver = instantiationService.createInstance(WindowDriver); const windowDriverChannel = new WindowDriverChannel(windowDriver); client.registerChannel('windowDriver', windowDriverChannel); @@ -224,9 +232,9 @@ export async function registerWindowDriver( const options = await windowDriverRegistry.registerWindowDriver(windowId); if (options.verbose) { - // windowDriver.openDevTools(); + windowDriver.openDevTools(); } const disposable = toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId)); return combinedDisposable([disposable, client]); -} \ No newline at end of file +} diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 46efb22aed5..ba2b3c673a2 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -21,13 +21,17 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' // TODO@joao: bad layering! import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; -import { NativeImage } from 'electron'; +import { toWinJsPromise } from 'vs/base/common/async'; class WindowRouter implements IClientRouter { constructor(private windowId: number) { } - route(command: string, arg: any): string { + routeCall(): string { + return `window:${this.windowId}`; + } + + routeEvent(): string { return `window:${this.windowId}`; } } @@ -50,57 +54,57 @@ export class Driver implements IDriver, IWindowDriverRegistry { @IWindowsMainService private windowsService: IWindowsMainService ) { } - async registerWindowDriver(windowId: number): TPromise { + registerWindowDriver(windowId: number): TPromise { this.registeredWindowIds.add(windowId); this.reloadingWindowIds.delete(windowId); this.onDidReloadingChange.fire(); - return this.options; + return TPromise.as(this.options); } - async reloadWindowDriver(windowId: number): TPromise { + reloadWindowDriver(windowId: number): TPromise { this.reloadingWindowIds.add(windowId); + return TPromise.as(null); } - async getWindowIds(): TPromise { - return this.windowsService.getWindows() + getWindowIds(): TPromise { + return TPromise.as(this.windowsService.getWindows() .map(w => w.id) - .filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id)); + .filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id))); } - async capturePage(windowId: number): TPromise { - await this.whenUnfrozen(windowId); - - const window = this.windowsService.getWindowById(windowId); - const webContents = window.win.webContents; - const image = await new Promise(c => webContents.capturePage(c)); - const buffer = image.toPNG(); - - return buffer.toString('base64'); + capturePage(windowId: number): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const window = this.windowsService.getWindowById(windowId); + const webContents = window.win.webContents; + return new TPromise(c => webContents.capturePage(image => c(image.toPNG().toString('base64')))); + }); } - async reloadWindow(windowId: number): TPromise { - await this.whenUnfrozen(windowId); - - const window = this.windowsService.getWindowById(windowId); - this.reloadingWindowIds.add(windowId); - this.windowsService.reload(window); + reloadWindow(windowId: number): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const window = this.windowsService.getWindowById(windowId); + this.reloadingWindowIds.add(windowId); + this.windowsService.reload(window); + }); } - async dispatchKeybinding(windowId: number, keybinding: string): TPromise { - await this.whenUnfrozen(windowId); + dispatchKeybinding(windowId: number, keybinding: string): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const [first, second] = KeybindingIO._readUserBinding(keybinding); - const [first, second] = KeybindingIO._readUserBinding(keybinding); - - await this._dispatchKeybinding(windowId, first); - - if (second) { - await this._dispatchKeybinding(windowId, second); - } + return this._dispatchKeybinding(windowId, first).then(() => { + if (second) { + return this._dispatchKeybinding(windowId, second); + } else { + return TPromise.as(null); + } + }); + }); } - private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise { + private _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise { if (keybinding instanceof ScanCodeBinding) { - throw new Error('ScanCodeBindings not supported'); + return TPromise.wrapError(new Error('ScanCodeBindings not supported')); } const window = this.windowsService.getWindowById(windowId); @@ -135,63 +139,76 @@ export class Driver implements IDriver, IWindowDriverRegistry { webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any); - await TPromise.timeout(100); + return TPromise.timeout(100); } - async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.click(selector, xoffset, yoffset); + click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.click(selector, xoffset, yoffset); + }); } - async doubleClick(windowId: number, selector: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.doubleClick(selector); + doubleClick(windowId: number, selector: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.doubleClick(selector); + }); } - async setValue(windowId: number, selector: string, text: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.setValue(selector, text); + setValue(windowId: number, selector: string, text: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.setValue(selector, text); + }); } - async getTitle(windowId: number): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.getTitle(); + getTitle(windowId: number): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.getTitle(); + }); } - async isActiveElement(windowId: number, selector: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.isActiveElement(selector); + isActiveElement(windowId: number, selector: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.isActiveElement(selector); + }); } - async getElements(windowId: number, selector: string, recursive: boolean): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.getElements(selector, recursive); + getElements(windowId: number, selector: string, recursive: boolean): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.getElements(selector, recursive); + }); } - async typeInEditor(windowId: number, selector: string, text: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.typeInEditor(selector, text); + typeInEditor(windowId: number, selector: string, text: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.typeInEditor(selector, text); + }); } - async getTerminalBuffer(windowId: number, selector: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.getTerminalBuffer(selector); + getTerminalBuffer(windowId: number, selector: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.getTerminalBuffer(selector); + }); } - async writeInTerminal(windowId: number, selector: string, text: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.writeInTerminal(selector, text); + writeInTerminal(windowId: number, selector: string, text: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.writeInTerminal(selector, text); + }); } - private async getWindowDriver(windowId: number): TPromise { - await this.whenUnfrozen(windowId); - - const router = new WindowRouter(windowId); - const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); - return new WindowDriverChannelClient(windowDriverChannel); + private getWindowDriver(windowId: number): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const router = new WindowRouter(windowId); + const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); + return new WindowDriverChannelClient(windowDriverChannel); + }); } - private async whenUnfrozen(windowId: number): TPromise { + private whenUnfrozen(windowId: number): TPromise { + return toWinJsPromise(this._whenUnfrozen(windowId)); + } + + private async _whenUnfrozen(windowId: number): Promise { while (this.reloadingWindowIds.has(windowId)) { await toPromise(this.onDidReloadingChange.event); } @@ -203,7 +220,7 @@ export async function serve( handle: string, environmentService: IEnvironmentService, instantiationService: IInstantiationService -): TPromise { +): Promise { const verbose = environmentService.driverVerbose; const driver = instantiationService.createInstance(Driver, windowServer, { verbose }); @@ -215,4 +232,4 @@ export async function serve( server.registerChannel('driver', channel); return combinedDisposable([server, windowServer]); -} \ No newline at end of file +} diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index fb61d1d1fa6..1f5df95b0c7 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -5,11 +5,10 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IDriver, DriverChannelClient } from 'vs/platform/driver/common/driver'; import { connect as connectNet, Client } from 'vs/base/parts/ipc/node/ipc.net'; -export async function connect(handle: string): TPromise<{ client: Client, driver: IDriver }> { +export async function connect(handle: string): Promise<{ client: Client, driver: IDriver }> { const client = await connectNet(handle, 'driverClient'); const channel = client.getChannel('driver'); const driver = new DriverChannelClient(channel); diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index d35396a181a..1f110f99f40 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -66,11 +66,11 @@ export interface IEditorOptions { readonly preserveFocus?: boolean; /** - * Tells the editor to replace the editor input in the editor even if it is identical to the one - * already showing. By default, the editor will not replace the input if it is identical to the + * Tells the editor to reload the editor input in the editor even if it is identical to the one + * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ - readonly forceOpen?: boolean; + readonly forceReload?: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. Note diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 06f80764726..99792a19f00 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -25,6 +25,7 @@ export interface ParsedArgs { performance?: boolean; 'prof-startup'?: string; 'prof-startup-prefix'?: string; + 'prof-append-timers'?: string; verbose?: boolean; log?: string; logExtensionHostCommunication?: boolean; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 882efb29b22..0aa4e7577a7 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -6,17 +6,17 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, LocalExtensionType, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension } from './extensionManagement'; import { Event, buffer, mapEvent } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; export interface IExtensionManagementChannel extends IChannel { - call(command: 'event:onInstallExtension'): TPromise; - call(command: 'event:onDidInstallExtension'): TPromise; - call(command: 'event:onUninstallExtension'): TPromise; - call(command: 'event:onDidUninstallExtension'): TPromise; + listen(event: 'onInstallExtension'): Event; + listen(event: 'onDidInstallExtension'): Event; + listen(event: 'onUninstallExtension'): Event; + listen(event: 'onDidUninstallExtension'): Event; call(command: 'install', args: [string]): TPromise; call(command: 'installFromGallery', args: [IGalleryExtension]): TPromise; call(command: 'uninstall', args: [ILocalExtension, boolean]): TPromise; @@ -33,28 +33,40 @@ export class ExtensionManagementChannel implements IExtensionManagementChannel { onUninstallExtension: Event; onDidUninstallExtension: Event; - constructor(private service: IExtensionManagementService) { + constructor(private service: IExtensionManagementService, private uriTransformer: IURITransformer) { this.onInstallExtension = buffer(service.onInstallExtension, true); this.onDidInstallExtension = buffer(service.onDidInstallExtension, true); this.onUninstallExtension = buffer(service.onUninstallExtension, true); this.onDidUninstallExtension = buffer(service.onDidUninstallExtension, true); } + listen(event: string): Event { + switch (event) { + case 'onInstallExtension': return this.onInstallExtension; + case 'onDidInstallExtension': return this.onDidInstallExtension; + case 'onUninstallExtension': return this.onUninstallExtension; + case 'onDidUninstallExtension': return this.onDidUninstallExtension; + } + + throw new Error('Invalid listen'); + } + call(command: string, args?: any): TPromise { switch (command) { - case 'event:onInstallExtension': return eventToCall(this.onInstallExtension); - case 'event:onDidInstallExtension': return eventToCall(this.onDidInstallExtension); - case 'event:onUninstallExtension': return eventToCall(this.onUninstallExtension); - case 'event:onDidUninstallExtension': return eventToCall(this.onDidUninstallExtension); case 'install': return this.service.install(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0]); - case 'uninstall': return this.service.uninstall(args[0], args[1]); - case 'reinstallFromGallery': return this.service.reinstallFromGallery(args[0]); + case 'uninstall': return this.service.uninstall(this._transform(args[0]), args[1]); + case 'reinstallFromGallery': return this.service.reinstallFromGallery(this._transform(args[0])); case 'getInstalled': return this.service.getInstalled(args[0]); - case 'updateMetadata': return this.service.updateMetadata(args[0], args[1]); + case 'updateMetadata': return this.service.updateMetadata(this._transform(args[0]), args[1]); case 'getExtensionsReport': return this.service.getExtensionsReport(); } - return undefined; + + throw new Error('Invalid call'); + } + + private _transform(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: URI.revive(this.uriTransformer.transformIncoming(extension.location)) } } : extension; } } @@ -64,17 +76,10 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer constructor(private channel: IExtensionManagementChannel, private uriTransformer: IURITransformer) { } - private _onInstallExtension = eventFromCall(this.channel, 'event:onInstallExtension'); - get onInstallExtension(): Event { return this._onInstallExtension; } - - private _onDidInstallExtension = mapEvent(eventFromCall(this.channel, 'event:onDidInstallExtension'), i => ({ ...i, local: this._transform(i.local) })); - get onDidInstallExtension(): Event { return this._onDidInstallExtension; } - - private _onUninstallExtension = eventFromCall(this.channel, 'event:onUninstallExtension'); - get onUninstallExtension(): Event { return this._onUninstallExtension; } - - private _onDidUninstallExtension = eventFromCall(this.channel, 'event:onDidUninstallExtension'); - get onDidUninstallExtension(): Event { return this._onDidUninstallExtension; } + get onInstallExtension(): Event { return this.channel.listen('onInstallExtension'); } + get onDidInstallExtension(): Event { return mapEvent(this.channel.listen('onDidInstallExtension'), i => ({ ...i, local: this._transform(i.local) })); } + get onUninstallExtension(): Event { return this.channel.listen('onUninstallExtension'); } + get onDidUninstallExtension(): Event { return this.channel.listen('onDidUninstallExtension'); } install(zipPath: string): TPromise { return this.channel.call('install', [zipPath]) diff --git a/src/vs/platform/integrity/node/integrityServiceImpl.ts b/src/vs/platform/integrity/node/integrityServiceImpl.ts index 5a45065e013..0113966b1cc 100644 --- a/src/vs/platform/integrity/node/integrityServiceImpl.ts +++ b/src/vs/platform/integrity/node/integrityServiceImpl.ts @@ -133,7 +133,7 @@ export class IntegrityServiceImpl implements IIntegrityService { private _resolve(filename: string, expected: string): TPromise { let fileUri = URI.parse(require.toUrl(filename)); - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { fs.readFile(fileUri.fsPath, (err, buff) => { if (err) { return e(err); diff --git a/src/vs/platform/issue/common/issueIpc.ts b/src/vs/platform/issue/common/issueIpc.ts index d804c6579fe..d45c1d870ee 100644 --- a/src/vs/platform/issue/common/issueIpc.ts +++ b/src/vs/platform/issue/common/issueIpc.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IIssueService, IssueReporterData, ProcessExplorerData } from './issue'; +import { Event } from 'vs/base/common/event'; export interface IIssueChannel extends IChannel { call(command: 'openIssueReporter', arg: IssueReporterData): TPromise; @@ -19,6 +20,10 @@ export class IssueChannel implements IIssueChannel { constructor(private service: IIssueService) { } + listen(event: string): Event { + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'openIssueReporter': diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index 05a43fe1992..cebb4e61237 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -78,8 +78,10 @@ export class IssueService implements IIssueService { this._issueWindow.on('close', () => this._issueWindow = null); this._issueParentWindow.on('closed', () => { - this._issueWindow.close(); - this._issueWindow = null; + if (this._issueWindow) { + this._issueWindow.close(); + this._issueWindow = null; + } }); } @@ -139,8 +141,10 @@ export class IssueService implements IIssueService { this._processExplorerWindow.on('close', () => this._processExplorerWindow = void 0); parentWindow.on('close', () => { - this._processExplorerWindow.close(); - this._processExplorerWindow = null; + if (this._processExplorerWindow) { + this._processExplorerWindow.close(); + this._processExplorerWindow = null; + } }); } diff --git a/src/vs/platform/localizations/common/localizationsIpc.ts b/src/vs/platform/localizations/common/localizationsIpc.ts index 51b6fb34147..2f06fb819ac 100644 --- a/src/vs/platform/localizations/common/localizationsIpc.ts +++ b/src/vs/platform/localizations/common/localizationsIpc.ts @@ -6,12 +6,14 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event, buffer } from 'vs/base/common/event'; import { ILocalizationsService, LanguageType } from 'vs/platform/localizations/common/localizations'; export interface ILocalizationsChannel extends IChannel { - call(command: 'event:onDidLanguagesChange'): TPromise; + listen(event: 'onDidLanguagesChange'): Event; + listen(event: string, arg?: any): Event; + call(command: 'getLanguageIds'): TPromise; call(command: string, arg?: any): TPromise; } @@ -24,9 +26,16 @@ export class LocalizationsChannel implements ILocalizationsChannel { this.onDidLanguagesChange = buffer(service.onDidLanguagesChange, true); } + listen(event: string): Event { + switch (event) { + case 'onDidLanguagesChange': return this.onDidLanguagesChange; + } + + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onDidLanguagesChange': return eventToCall(this.onDidLanguagesChange); case 'getLanguageIds': return this.service.getLanguageIds(arg); } return undefined; @@ -39,8 +48,7 @@ export class LocalizationsChannelClient implements ILocalizationsService { constructor(private channel: ILocalizationsChannel) { } - private _onDidLanguagesChange = eventFromCall(this.channel, 'event:onDidLanguagesChange'); - get onDidLanguagesChange(): Event { return this._onDidLanguagesChange; } + get onDidLanguagesChange(): Event { return this.channel.listen('onDidLanguagesChange'); } getLanguageIds(type?: LanguageType): TPromise { return this.channel.call('getLanguageIds', type); diff --git a/src/vs/platform/localizations/node/localizations.ts b/src/vs/platform/localizations/node/localizations.ts index 2e7b180fc83..46bd114b4cf 100644 --- a/src/vs/platform/localizations/node/localizations.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -28,7 +28,7 @@ interface ILanguagePack { translations: { [id: string]: string }; } -const systemLanguages: string[] = ['de', 'en', 'en-US', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh-CN', 'zh-Hans', 'zh-TW', 'zh-Hant']; +const systemLanguages: string[] = ['de', 'en', 'en-US', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh-CN', 'zh-TW']; if (product.quality !== 'stable') { systemLanguages.push('hu'); } diff --git a/src/vs/platform/log/common/logIpc.ts b/src/vs/platform/log/common/logIpc.ts index d39072ea674..24bccff9463 100644 --- a/src/vs/platform/log/common/logIpc.ts +++ b/src/vs/platform/log/common/logIpc.ts @@ -3,14 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { TPromise } from 'vs/base/common/winjs.base'; import { LogLevel, ILogService, DelegatedLogService } from 'vs/platform/log/common/log'; import { Event, buffer } from 'vs/base/common/event'; export interface ILogLevelSetterChannel extends IChannel { - call(command: 'event:onDidChangeLogLevel'): TPromise; + listen(event: 'onDidChangeLogLevel'): Event; + listen(event: string, arg?: any): Event; + call(command: 'setLevel', logLevel: LogLevel): TPromise; + call(command: string, arg?: any): TPromise; } export class LogLevelSetterChannel implements ILogLevelSetterChannel { @@ -21,9 +24,16 @@ export class LogLevelSetterChannel implements ILogLevelSetterChannel { this.onDidChangeLogLevel = buffer(service.onDidChangeLogLevel, true); } + listen(event: string): Event { + switch (event) { + case 'onDidChangeLogLevel': return this.onDidChangeLogLevel; + } + + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onDidChangeLogLevel': return eventToCall(this.onDidChangeLogLevel); case 'setLevel': this.service.setLevel(arg); return TPromise.as(null); } return undefined; @@ -34,8 +44,9 @@ export class LogLevelSetterChannelClient { constructor(private channel: ILogLevelSetterChannel) { } - private _onDidChangeLogLevel = eventFromCall(this.channel, 'event:onDidChangeLogLevel'); - get onDidChangeLogLevel(): Event { return this._onDidChangeLogLevel; } + get onDidChangeLogLevel(): Event { + return this.channel.listen('onDidChangeLogLevel'); + } setLevel(level: LogLevel): TPromise { return this.channel.call('setLevel', level); diff --git a/src/vs/platform/menubar/common/menubarIpc.ts b/src/vs/platform/menubar/common/menubarIpc.ts index 52b2841d3e8..13699176ef5 100644 --- a/src/vs/platform/menubar/common/menubarIpc.ts +++ b/src/vs/platform/menubar/common/menubarIpc.ts @@ -7,6 +7,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { TPromise } from 'vs/base/common/winjs.base'; import { IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; +import { Event } from 'vs/base/common/event'; export interface IMenubarChannel extends IChannel { call(command: 'updateMenubar', arg: [number, IMenubarData]): TPromise; @@ -17,6 +18,10 @@ export class MenubarChannel implements IMenubarChannel { constructor(private service: IMenubarService) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'updateMenubar': return this.service.updateMenubar(arg[0], arg[1]); diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index a1fca9abef9..405170fa1e1 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; @@ -24,7 +23,7 @@ export interface IProgressService { * Indicate progress for the duration of the provided promise. Progress will stop in * any case of promise completion, error or cancellation. */ - showWhile(promise: TPromise, delay?: number): TPromise; + showWhile(promise: Thenable, delay?: number): Thenable; } export interface IProgressRunner { @@ -126,4 +125,4 @@ export class LongRunningOperation { dispose(): void { this.currentOperationDisposables = dispose(this.currentOperationDisposables); } -} \ No newline at end of file +} diff --git a/src/vs/platform/quickOpen/common/quickOpen.ts b/src/vs/platform/quickOpen/common/quickOpen.ts index e5e1bffd9d4..6a9b847fc09 100644 --- a/src/vs/platform/quickOpen/common/quickOpen.ts +++ b/src/vs/platform/quickOpen/common/quickOpen.ts @@ -86,6 +86,14 @@ export interface IPickOptions { contextKey?: string; } +export interface IStringPickOptions extends IPickOptions { + onDidFocus?: (item: string) => void; +} + +export interface ITypedPickOptions extends IPickOptions { + onDidFocus?: (entry: T) => void; +} + export interface IShowOptions { quickNavigateConfiguration?: IQuickNavigateConfiguration; inputSelection?: { start: number; end: number; }; @@ -114,10 +122,10 @@ export interface IQuickOpenService { * Passing in a promise will allow you to resolve the elements in the background while quick open will show a * progress bar spinning. */ - pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise; + pick(picks: TPromise, options?: IStringPickOptions, token?: CancellationToken): TPromise; + pick(picks: TPromise, options?: ITypedPickOptions, token?: CancellationToken): TPromise; + pick(picks: string[], options?: IStringPickOptions, token?: CancellationToken): TPromise; + pick(picks: T[], options?: ITypedPickOptions, token?: CancellationToken): TPromise; /** * Allows to navigate from the outside in an opened picker. diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 12a15db8b3c..0ec12af97a1 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -23,7 +23,7 @@ export interface IQuickNavigateConfiguration { keybindings: ResolvedKeybinding[]; } -export interface IPickOptions { +export interface IPickOptions { /** * an optional string to show as place holder in the input box to guide the user what she picks on @@ -49,6 +49,8 @@ export interface IPickOptions { * an optional flag to make this picker multi-select */ canPickMany?: boolean; + + onDidFocus?: (entry: T) => void; } export interface IInputOptions { @@ -177,7 +179,7 @@ export interface IQuickInputService { /** * Opens the quick input box for selecting items and returns a promise with the user selected item(s) if any. */ - pick(picks: TPromise, options?: O, token?: CancellationToken): TPromise; + pick>(picks: TPromise, options?: O, token?: CancellationToken): TPromise; /** * Opens the quick input box for text input and returns a promise with the user typed value if any. diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 2e1e6dcab87..91ad26c83c2 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -47,6 +47,7 @@ export interface ISearchHistoryService { export interface ISearchResultProvider { search(query: ISearchQuery): PPromise; + clearCache(cacheKey: string): TPromise; } export interface IFolderQuery { diff --git a/src/vs/platform/telemetry/common/telemetryIpc.ts b/src/vs/platform/telemetry/common/telemetryIpc.ts index df3b884b3ed..f08e0c0969c 100644 --- a/src/vs/platform/telemetry/common/telemetryIpc.ts +++ b/src/vs/platform/telemetry/common/telemetryIpc.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { Event } from 'vs/base/common/event'; export interface ITelemetryLog { eventName: string; @@ -23,6 +24,10 @@ export class TelemetryAppenderChannel implements ITelemetryAppenderChannel { constructor(private appender: ITelemetryAppender) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, { eventName, data }: ITelemetryLog): TPromise { this.appender.log(eventName, data); return TPromise.as(null); diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 07dfd1d9551..a14be6f3154 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -261,4 +261,34 @@ export function attachProgressBarStyler(widget: IThemable, themeService: IThemeS export function attachStylerCallback(themeService: IThemeService, colors: { [name: string]: ColorIdentifier }, callback: styleFn): IDisposable { return attachStyler(themeService, colors, callback); -} \ No newline at end of file +} + +export interface IBreadcrumbsWidgetStyleOverrides extends IStyleOverrides { + breadcrumbsBackground?: ColorIdentifier; + breadcrumbsItemHoverBackground?: ColorIdentifier; + breadcrumbsItemHoverForeground?: ColorIdentifier; + breadcrumbsItemFocusBackground?: ColorIdentifier; + breadcrumbsItemFocusForeground?: ColorIdentifier; + breadcrumbsActiveItemSelectionBackground?: ColorIdentifier; + breadcrumbsActiveItemSelectionForeground?: ColorIdentifier; + breadcrumbsInactiveItemSelectionBackground?: ColorIdentifier; + breadcrumbsInactiveItemSelectionForeground?: ColorIdentifier; +} + +export const defaultBreadcrumbsStyles = { + breadcrumbsBackground: editorBackground, + breadcrumbsItemHoverBackground: listHoverBackground, + breadcrumbsItemHoverForeground: listHoverForeground, + breadcrumbsItemFocusBackground: listFocusBackground, + breadcrumbsItemFocusForeground: listFocusForeground, + breadcrumbsItemSelectionBackground: listActiveSelectionBackground, + breadcrumbsItemSelectionForeground: listActiveSelectionForeground, + breadcrumbsActiveItemSelectionBackground: listActiveSelectionBackground, + breadcrumbsActiveItemSelectionForeground: listActiveSelectionForeground, + breadcrumbsInactiveItemSelectionBackground: editorBackground, + breadcrumbsInactiveItemSelectionForeground: listInactiveSelectionForeground, +}; + +export function attachBreadcrumbsStyler(widget: IThemable, themeService: IThemeService, style?: IBreadcrumbsWidgetStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultBreadcrumbsStyles, ...style }, widget); +} diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index 5138e17702c..aa2e8fd304e 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -6,12 +6,15 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event, Emitter } from 'vs/base/common/event'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IUpdateService, State } from './update'; export interface IUpdateChannel extends IChannel { + listen(event: 'onStateChange'): Event; + listen(command: string, arg?: any): Event; + call(command: 'checkForUpdates', arg: any): TPromise; call(command: 'downloadUpdate'): TPromise; call(command: 'applyUpdate'): TPromise; @@ -25,9 +28,16 @@ export class UpdateChannel implements IUpdateChannel { constructor(private service: IUpdateService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'onStateChange': return this.service.onStateChange; + } + + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onStateChange': return eventToCall(this.service.onStateChange); case 'checkForUpdates': return this.service.checkForUpdates(arg); case 'downloadUpdate': return this.service.downloadUpdate(); case 'applyUpdate': return this.service.applyUpdate(); @@ -43,8 +53,6 @@ export class UpdateChannelClient implements IUpdateService { _serviceBrand: any; - private _onRemoteStateChange = eventFromCall(this.channel, 'event:onStateChange'); - private _onStateChange = new Emitter(); get onStateChange(): Event { return this._onStateChange.event; } @@ -60,7 +68,8 @@ export class UpdateChannelClient implements IUpdateService { this._onStateChange.fire(state); // fire subsequent states as they come in from remote - this._onRemoteStateChange(state => this._onStateChange.fire(state)); + + this.channel.listen('onStateChange')(state => this._onStateChange.fire(state)); }, onUnexpectedError); } diff --git a/src/vs/platform/url/common/urlIpc.ts b/src/vs/platform/url/common/urlIpc.ts index 0cc9ec695ad..84fdc5c2e65 100644 --- a/src/vs/platform/url/common/urlIpc.ts +++ b/src/vs/platform/url/common/urlIpc.ts @@ -10,6 +10,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IURLHandler, IURLService } from './url'; import URI from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; export interface IURLServiceChannel extends IChannel { call(command: 'open', url: string): TPromise; @@ -20,6 +21,10 @@ export class URLServiceChannel implements IURLServiceChannel { constructor(private service: IURLService) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'open': return this.service.open(URI.revive(arg)); @@ -52,6 +57,10 @@ export class URLHandlerChannel implements IURLHandlerChannel { constructor(private handler: IURLHandler) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'handleURL': return this.handler.handleURL(URI.revive(arg)); diff --git a/src/vs/platform/url/common/urlService.ts b/src/vs/platform/url/common/urlService.ts index f4fbab19d7d..2d7fda8b99f 100644 --- a/src/vs/platform/url/common/urlService.ts +++ b/src/vs/platform/url/common/urlService.ts @@ -9,6 +9,7 @@ import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import URI from 'vs/base/common/uri'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; +import { first } from 'vs/base/common/async'; declare module Array { function from(set: Set): T[]; @@ -20,16 +21,9 @@ export class URLService implements IURLService { private handlers = new Set(); - async open(uri: URI): TPromise { + open(uri: URI): TPromise { const handlers = Array.from(this.handlers); - - for (const handler of handlers) { - if (await handler.handleURL(uri)) { - return true; - } - } - - return false; + return first(handlers.map(h => () => h.handleURL(uri)), undefined, false); } registerHandler(handler: IURLHandler): IDisposable { @@ -44,7 +38,7 @@ export class RelayURLService extends URLService implements IURLHandler { super(); } - async open(uri: URI): TPromise { + open(uri: URI): TPromise { return this.urlService.open(uri); } diff --git a/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts b/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts index f8c42720026..ffc232d2f09 100644 --- a/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts +++ b/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts @@ -57,56 +57,57 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { ]); } - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { if (!isExtensionId(uri.authority)) { - return false; + return TPromise.as(false); } const extensionId = uri.authority; const wasHandlerAvailable = this.extensionHandlers.has(extensionId); - const extensions = await this.extensionService.getExtensions(); - const extension = extensions.filter(e => e.id === extensionId)[0]; + return this.extensionService.getExtensions().then(extensions => { + const extension = extensions.filter(e => e.id === extensionId)[0]; - if (!extension) { - return false; - } - - const result = await this.dialogService.confirm({ - message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId), - detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}` - }); - - if (!result.confirmed) { - return true; - } - - const handler = this.extensionHandlers.get(extensionId); - if (handler) { - if (!wasHandlerAvailable) { - // forward it directly - return handler.handleURL(uri); + if (!extension) { + return TPromise.as(false); } - // let the ExtensionUrlHandler instance handle this - return TPromise.as(false); - } + return this.dialogService.confirm({ + message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId), + detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}` + }).then(result => { - // collect URI for eventual extension activation - const timestamp = new Date().getTime(); - let uris = this.uriBuffer.get(extensionId); + if (!result.confirmed) { + return TPromise.as(true); + } - if (!uris) { - uris = []; - this.uriBuffer.set(extensionId, uris); - } + const handler = this.extensionHandlers.get(extensionId); + if (handler) { + if (!wasHandlerAvailable) { + // forward it directly + return handler.handleURL(uri); + } - uris.push({ timestamp, uri }); + // let the ExtensionUrlHandler instance handle this + return TPromise.as(false); + } - // activate the extension - await this.extensionService.activateByEvent(`onUri:${extensionId}`); + // collect URI for eventual extension activation + const timestamp = new Date().getTime(); + let uris = this.uriBuffer.get(extensionId); - return true; + if (!uris) { + uris = []; + this.uriBuffer.set(extensionId, uris); + } + + uris.push({ timestamp, uri }); + + // activate the extension + return this.extensionService.activateByEvent(`onUri:${extensionId}`) + .then(() => true); + }); + }); } registerExtensionHandler(extensionId: string, handler: IURLHandler): void { diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index 8875c105621..0adca2f73b4 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event, buffer } from 'vs/base/common/event'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, CrashReporterStartOptions, IMessageBoxResult, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IDevToolsOptions } from 'vs/platform/windows/common/windows'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; @@ -16,10 +16,14 @@ import URI from 'vs/base/common/uri'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; export interface IWindowsChannel extends IChannel { - call(command: 'event:onWindowOpen'): TPromise; - call(command: 'event:onWindowFocus'): TPromise; - call(command: 'event:onWindowBlur'): TPromise; - call(command: 'event:onRecentlyOpenedChange'): TPromise; + listen(event: 'onWindowOpen'): Event; + listen(event: 'onWindowFocus'): Event; + listen(event: 'onWindowBlur'): Event; + listen(event: 'onWindowMaximize'): Event; + listen(event: 'onWindowUnmaximize'): Event; + listen(event: 'onRecentlyOpenedChange'): Event; + listen(event: string, arg?: any): Event; + call(command: 'pickFileFolderAndOpen', arg: INativeOpenDialogOptions): TPromise; call(command: 'pickFileAndOpen', arg: INativeOpenDialogOptions): TPromise; call(command: 'pickFolderAndOpen', arg: INativeOpenDialogOptions): TPromise; @@ -89,14 +93,21 @@ export class WindowsChannel implements IWindowsChannel { this.onRecentlyOpenedChange = buffer(service.onRecentlyOpenedChange, true); } + listen(event: string, arg?: any): Event { + switch (event) { + case 'onWindowOpen': return this.onWindowOpen; + case 'onWindowFocus': return this.onWindowFocus; + case 'onWindowBlur': return this.onWindowBlur; + case 'onWindowMaximize': return this.onWindowMaximize; + case 'onWindowUnmaximize': return this.onWindowUnmaximize; + case 'onRecentlyOpenedChange': return this.onRecentlyOpenedChange; + } + + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onWindowOpen': return eventToCall(this.onWindowOpen); - case 'event:onWindowFocus': return eventToCall(this.onWindowFocus); - case 'event:onWindowBlur': return eventToCall(this.onWindowBlur); - case 'event:onWindowMaximize': return eventToCall(this.onWindowMaximize); - case 'event:onWindowUnmaximize': return eventToCall(this.onWindowUnmaximize); - case 'event:onRecentlyOpenedChange': return eventToCall(this.onRecentlyOpenedChange); case 'pickFileFolderAndOpen': return this.service.pickFileFolderAndOpen(arg); case 'pickFileAndOpen': return this.service.pickFileAndOpen(arg); case 'pickFolderAndOpen': return this.service.pickFolderAndOpen(arg); @@ -170,23 +181,12 @@ export class WindowsChannelClient implements IWindowsService { constructor(private channel: IWindowsChannel) { } - private _onWindowOpen: Event = eventFromCall(this.channel, 'event:onWindowOpen'); - get onWindowOpen(): Event { return this._onWindowOpen; } - - private _onWindowFocus: Event = eventFromCall(this.channel, 'event:onWindowFocus'); - get onWindowFocus(): Event { return this._onWindowFocus; } - - private _onWindowBlur: Event = eventFromCall(this.channel, 'event:onWindowBlur'); - get onWindowBlur(): Event { return this._onWindowBlur; } - - private _onWindowMaximize: Event = eventFromCall(this.channel, 'event:onWindowMaximize'); - get onWindowMaximize(): Event { return this._onWindowMaximize; } - - private _onWindowUnmaximize: Event = eventFromCall(this.channel, 'event:onWindowUnmaximize'); - get onWindowUnmaximize(): Event { return this._onWindowUnmaximize; } - - private _onRecentlyOpenedChange: Event = eventFromCall(this.channel, 'event:onRecentlyOpenedChange'); - get onRecentlyOpenedChange(): Event { return this._onRecentlyOpenedChange; } + get onWindowOpen(): Event { return this.channel.listen('onWindowOpen'); } + get onWindowFocus(): Event { return this.channel.listen('onWindowFocus'); } + get onWindowBlur(): Event { return this.channel.listen('onWindowBlur'); } + get onWindowMaximize(): Event { return this.channel.listen('onWindowMaximize'); } + get onWindowUnmaximize(): Event { return this.channel.listen('onWindowUnmaximize'); } + get onRecentlyOpenedChange(): Event { return this.channel.listen('onRecentlyOpenedChange'); } pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { return this.channel.call('pickFileFolderAndOpen', options); diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index d9bbd5c7c07..2987443c204 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -530,21 +530,22 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { // Catch file URLs if (uri.authority === Schemas.file && !!uri.path) { - return this.openFileForURI(URI.file(uri.fsPath)); + this.openFileForURI(URI.file(uri.fsPath)); + return TPromise.as(true); } - return false; + return TPromise.wrap(false); } - private async openFileForURI(uri: URI): TPromise { + private openFileForURI(uri: URI): TPromise { const cli = assign(Object.create(null), this.environmentService.args, { goto: true }); const pathsToOpen = [uri.fsPath]; this.windowsMainService.open({ context: OpenContext.API, cli, pathsToOpen }); - return true; + return TPromise.wrap(true); } dispose(): void { diff --git a/src/vs/platform/workspaces/common/workspacesIpc.ts b/src/vs/platform/workspaces/common/workspacesIpc.ts index 898f9d43a29..e123da0b4c7 100644 --- a/src/vs/platform/workspaces/common/workspacesIpc.ts +++ b/src/vs/platform/workspaces/common/workspacesIpc.ts @@ -9,6 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IWorkspacesService, IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import URI from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; export interface IWorkspacesChannel extends IChannel { call(command: 'createWorkspace', arg: [IWorkspaceFolderCreationData[]]): TPromise; @@ -19,6 +20,10 @@ export class WorkspacesChannel implements IWorkspacesChannel { constructor(private service: IWorkspacesMainService) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'createWorkspace': { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 10e2b712d23..27a09386ff7 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3948,9 +3948,9 @@ declare module 'vscode' { * * Diagnostics with this tag are rendered faded out. The amount of fading * is controlled by the `"editorUnnecessaryCode.opacity"` theme color. For - * example, `"editorUnnecessaryCode.opacity": "#000000c0" will render the + * example, `"editorUnnecessaryCode.opacity": "#000000c0"` will render the * code with 75% opacity. For high contrast themes, use the - * `"editorUnnecessaryCode.border"` the color to underline unnecessary code + * `"editorUnnecessaryCode.border"` theme color to underline unnecessary code * instead of fading it out. */ Unnecessary = 1, @@ -5732,6 +5732,21 @@ declare module 'vscode' { readonly focused: boolean; } + /** + * A uri handler is responsible for handling system-wide [uris](#Uri). + * + * @see [window.registerUriHandler](#window.registerUriHandler). + */ + export interface UriHandler { + + /** + * Handle the provided system-wide [uri](#Uri). + * + * @see [window.registerUriHandler](#window.registerUriHandler). + */ + handleUri(uri: Uri): ProviderResult; + } + /** * Namespace for dealing with the current window of the editor. That is visible * and active editors, as well as, UI elements to show messages, selections, and @@ -5784,6 +5799,17 @@ declare module 'vscode' { */ export const onDidChangeTextEditorViewColumn: Event; + /** + * The currently opened terminals or an empty array. + */ + export const terminals: ReadonlyArray; + + /** + * An [event](#Event) which fires when a terminal has been created, either through the + * [createTerminal](#window.createTerminal) API or commands. + */ + export const onDidOpenTerminal: Event; + /** * An [event](#Event) which fires when a terminal is disposed. */ @@ -6192,6 +6218,29 @@ declare module 'vscode' { */ export function createTreeView(viewId: string, options: { treeDataProvider: TreeDataProvider }): TreeView; + /** + * Registers a [uri handler](#UriHandler) capable of handling system-wide [uris](#Uri). + * In case there are multiple windows open, the topmost window will handle the uri. + * A uri handler is scoped to the extension it is contributed from; it will only + * be able to handle uris which are directed to the extension itself. A uri must respect + * the following rules: + * + * - The uri-scheme must be the product name; + * - The uri-authority must be the extension id (eg. `my.extension`); + * - The uri-path, -query and -fragment parts are arbitrary. + * + * For example, if the `my.extension` extension registers a uri handler, it will only + * be allowed to handle uris with the prefix `product-name://my.extension`. + * + * An extension can only register a single uri handler in its entire activation lifetime. + * + * * *Note:* There is an activation event `onUri` that fires when a uri directed for + * the current extension is about to be handled. + * + * @param handler The uri handler to register for this extension. + */ + export function registerUriHandler(handler: UriHandler): Disposable; + /** * Registers a webview panel serializer. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 73007519c97..6dcc58cc6df 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -13,47 +13,185 @@ declare module 'vscode' { export function sampleFunction(): Thenable; } - //#region Joh: remote, search provider + //#region Rob: search provider + /** + * The parameters of a query for text search. + */ export interface TextSearchQuery { + /** + * The text pattern to search for. + */ pattern: string; + + /** + * Whether or not `pattern` should be interpreted as a regular expression. + */ isRegExp?: boolean; + + /** + * Whether or not the search should be case-sensitive. + */ isCaseSensitive?: boolean; + + /** + * Whether or not to search for whole word matches only. + */ isWordMatch?: boolean; } + /** + * A file glob pattern to match file paths against. + * TODO@roblou - merge this with the GlobPattern docs/definition in vscode.d.ts. + * @see [GlobPattern](#GlobPattern) + */ + export type GlobString = string; + + /** + * Options common to file and text search + */ export interface SearchOptions { + /** + * The root folder to search within. + */ folder: Uri; - includes: string[]; // paths relative to folder - excludes: string[]; + + /** + * Files that match an `includes` glob pattern should be included in the search. + */ + includes: GlobString[]; + + /** + * Files that match an `excludes` glob pattern should be excluded from the search. + */ + excludes: GlobString[]; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ useIgnoreFiles?: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ followSymlinks?: boolean; + + /** + * The maximum number of results to be returned. + */ + maxResults?: number; } + /** + * Options that apply to text search. + */ export interface TextSearchOptions extends SearchOptions { - previewOptions?: any; // total length? # of context lines? leading and trailing # of chars? + /** + * TODO@roblou - total length? # of context lines? leading and trailing # of chars? + */ + previewOptions?: any; + + /** + * Exclude files larger than `maxFileSize` in bytes. + */ maxFileSize?: number; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ encoding?: string; } - export interface FileSearchOptions extends SearchOptions { } + /** + * The parameters of a query for file search. + */ + export interface FileSearchQuery { + /** + * The search pattern to match against file paths. + */ + pattern: string; - export interface TextSearchResult { - path: string; - range: Range; - - // For now, preview must be a single line of text - preview: { text: string, match: Range }; + /** + * `cacheKey` has the same value when `provideFileSearchResults` is invoked multiple times during a single quickopen session. + * Providers can optionally use this to cache results at the beginning of a quickopen session and filter results as the user types. + * It will have a different value for each folder searched. + */ + cacheKey?: string; } + /** + * Options that apply to file search. + */ + export interface FileSearchOptions extends SearchOptions { } + + export interface TextSearchResultPreview { + /** + * The matching line of text, or a portion of the matching line that contains the match. + * For now, this can only be a single line. + */ + text: string; + + /** + * The Range within `text` corresponding to the text of the match. + */ + match: Range; + } + + /** + * A match from a text search + */ + export interface TextSearchResult { + /** + * The uri for the matching document. + */ + uri: Uri; + + /** + * The range of the match within the document. + */ + range: Range; + + /** + * A preview of the matching line + */ + preview: TextSearchResultPreview; + } + + /** + * A SearchProvider provides search results for files or text in files. It can be invoked by quickopen, the search viewlet, and other extensions. + */ export interface SearchProvider { - provideFileSearchResults?(options: FileSearchOptions, progress: Progress, token: CancellationToken): Thenable; + /** + * Provide the set of files that match a certain file path pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching files. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideFileSearchResults?(query: FileSearchQuery, options: FileSearchOptions, progress: Progress, token: CancellationToken): Thenable; + + /** + * Optional - if the provider makes use of `query.cacheKey`, it can implement this method which is invoked when the cache can be cleared. + * @param cacheKey The same key that was passed as `query.cacheKey`. + */ + clearCache?(cacheKey: string): void; + + /** + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ provideTextSearchResults?(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; } export interface FindTextInFilesOptions { - includes?: GlobPattern[]; - excludes?: GlobPattern[]; + include?: GlobPattern; + exclude?: GlobPattern; maxResults?: number; useIgnoreFiles?: boolean; followSymlinks?: boolean; @@ -61,7 +199,17 @@ declare module 'vscode' { } export namespace workspace { + /** + * Register a search provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ export function registerSearchProvider(scheme: string, provider: SearchProvider): Disposable; + export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; } @@ -467,11 +615,6 @@ declare module 'vscode' { } export namespace window { - /** - * The currently opened terminals or an empty array. - */ - export const terminals: ReadonlyArray; - /** * The currently active terminal or `undefined`. The active terminal is the one that * currently has focus or most recently had focus. @@ -485,12 +628,6 @@ declare module 'vscode' { */ export const onDidChangeActiveTerminal: Event; - /** - * An [event](#Event) which fires when a terminal has been created, either through the - * [createTerminal](#window.createTerminal) API or commands. - */ - export const onDidOpenTerminal: Event; - /** * Create a [TerminalRenderer](#TerminalRenderer). * @@ -501,22 +638,6 @@ declare module 'vscode' { //#endregion - //#region URLs - - export interface ProtocolHandler { - handleUri(uri: Uri): void; - } - - export namespace window { - - /** - * Registers a protocol handler capable of handling system-wide URIs. - */ - export function registerProtocolHandler(handler: ProtocolHandler): Disposable; - } - - //#endregion - //#region Joh -> exclusive document filters export interface DocumentFilter { @@ -542,7 +663,7 @@ declare module 'vscode' { * of items of type T. * * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) - * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used, + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. * * @return A new [QuickPick](#QuickPick). @@ -553,7 +674,7 @@ declare module 'vscode' { * Creates a [InputBox](#InputBox) to let the user enter some text input. * * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) - * is easier to use. [window.createInputBox](#window.createInputBox) should be used, + * is easier to use. [window.createInputBox](#window.createInputBox) should be used * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. * * @return A new [InputBox](#InputBox). @@ -658,7 +779,7 @@ declare module 'vscode' { * selecting multiple items. * * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) - * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used, + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. */ export interface QuickPick extends QuickInput { @@ -738,7 +859,7 @@ declare module 'vscode' { * A concrete [QuickInput](#QuickInput) to let the user input a text value. * * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) - * is easier to use. [window.createInputBox](#window.createInputBox) should be used, + * is easier to use. [window.createInputBox](#window.createInputBox) should be used * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. */ export interface InputBox extends QuickInput { diff --git a/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts b/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts index 1f8d8af2c36..4cef3fd8b1b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts @@ -10,14 +10,14 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainContext, IExtHostContext, MainThreadDecorationsShape, ExtHostDecorationsShape, DecorationData, DecorationRequest } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IDecorationsService, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; -import { TPromise } from 'vs/base/common/winjs.base'; import { values } from 'vs/base/common/collections'; +import { CancellationToken } from 'vs/base/common/cancellation'; class DecorationRequestsQueue { private _idPool = 0; private _requests: { [id: number]: DecorationRequest } = Object.create(null); - private _resolver: { [id: number]: Function } = Object.create(null); + private _resolver: { [id: number]: (data: DecorationData) => any } = Object.create(null); private _timer: number; @@ -27,16 +27,18 @@ class DecorationRequestsQueue { // } - enqueue(handle: number, uri: URI): TPromise { + enqueue(handle: number, uri: URI, token: CancellationToken): Promise { const id = ++this._idPool; - return new TPromise((resolve, reject) => { + const result = new Promise(resolve => { this._requests[id] = { id, handle, uri }; this._resolver[id] = resolve; this._processQueue(); - }, () => { + }); + token.onCancellationRequested(() => { delete this._requests[id]; delete this._resolver[id]; }); + return result; } private _processQueue(): void { @@ -87,8 +89,8 @@ export class MainThreadDecorations implements MainThreadDecorationsShape { const registration = this._decorationsService.registerDecorationsProvider({ label, onDidChange: emitter.event, - provideDecorations: (uri) => { - return this._requestQueue.enqueue(handle, uri).then(data => { + provideDecorations: (uri, token) => { + return this._requestQueue.enqueue(handle, uri, token).then(data => { if (!data) { return undefined; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts b/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts index ccf9eeb2637..dfd5a60757e 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts @@ -4,17 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable } from 'vs/base/common/lifecycle'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ITextModel, DefaultEndOfLine } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { MainThreadDocumentContentProvidersShape, ExtHostContext, ExtHostDocumentContentProvidersShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; -import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { ExtHostContext, ExtHostDocumentContentProvidersShape, IExtHostContext, MainContext, MainThreadDocumentContentProvidersShape } from '../node/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadDocumentContentProviders) export class MainThreadDocumentContentProviders implements MainThreadDocumentContentProvidersShape { @@ -27,6 +30,7 @@ export class MainThreadDocumentContentProviders implements MainThreadDocumentCon @ITextModelService private readonly _textModelResolverService: ITextModelService, @IModeService private readonly _modeService: IModeService, @IModelService private readonly _modelService: IModelService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ICodeEditorService codeEditorService: ICodeEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentContentProviders); @@ -67,10 +71,11 @@ export class MainThreadDocumentContentProviders implements MainThreadDocumentCon return; } - const textBuffer = createTextBuffer(value, DefaultEndOfLine.CRLF); - - if (!model.equalsTextBuffer(textBuffer)) { - model.setValueFromTextBuffer(textBuffer); - } + this._editorWorkerService.computeMoreMinimalEdits(model.uri, [{ text: value, range: model.getFullModelRange() }]).then(edits => { + if (edits.length > 0) { + // use the evil-edit as these models show in readonly-editor only + model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); + } + }, onUnexpectedError); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts index 5afe23566d3..7435dda07b6 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts @@ -38,7 +38,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { public dispose(): void { } - $show(options: IPickOptions): TPromise { + $show(options: IPickOptions): TPromise { const myToken = ++this._token; this._contents = new TPromise((c, e) => { @@ -55,16 +55,21 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { }; }); + options = { + ...options, + onDidFocus: el => { + if (el) { + this._proxy.$onItemSelected((el).handle); + } + } + }; + if (options.canPickMany) { return asWinJsPromise(token => this._quickInputService.pick(this._contents, options as { canPickMany: true }, token)).then(items => { if (items) { return items.map(item => item.handle); } return undefined; - }, undefined, progress => { - if (progress) { - this._proxy.$onItemSelected((progress).handle); - } }); } else { return asWinJsPromise(token => this._quickInputService.pick(this._contents, options, token)).then(item => { @@ -72,10 +77,6 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { return item.handle; } return undefined; - }, undefined, progress => { - if (progress) { - this._proxy.$onItemSelected((progress).handle); - } }); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index 6bd43da77dc..8ee8daaee7a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts @@ -401,17 +401,17 @@ export class MainThreadSCM implements MainThreadSCMShape { } if (enabled) { - repository.input.validateInput = async (value, pos): TPromise => { - const result = await this._proxy.$validateInput(sourceControlHandle, value, pos); + repository.input.validateInput = (value, pos): TPromise => { + return this._proxy.$validateInput(sourceControlHandle, value, pos).then(result => { + if (!result) { + return undefined; + } - if (!result) { - return undefined; - } - - return { - message: result[0], - type: result[1] - }; + return { + message: result[0], + type: result[1] + }; + }); }; } else { repository.input.validateInput = () => TPromise.as(undefined); diff --git a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts index 6e692f064b7..7e3224c1de2 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts @@ -29,7 +29,7 @@ export class MainThreadSearch implements MainThreadSearchShape { } dispose(): void { - this._searchProvider.forEach(value => dispose()); + this._searchProvider.forEach(value => value.dispose()); this._searchProvider.clear(); } @@ -42,7 +42,11 @@ export class MainThreadSearch implements MainThreadSearchShape { this._searchProvider.delete(handle); } - $handleFindMatch(handle: number, session, data: UriComponents | IRawFileMatch2[]): void { + $handleFileMatch(handle: number, session, data: UriComponents[]): void { + this._searchProvider.get(handle).handleFindMatch(session, data); + } + + $handleTextMatch(handle: number, session, data: IRawFileMatch2[]): void { this._searchProvider.get(handle).handleFindMatch(session, data); } @@ -75,7 +79,7 @@ class SearchOperation { } } -class RemoteSearchProvider implements ISearchResultProvider { +class RemoteSearchProvider implements ISearchResultProvider, IDisposable { private readonly _registrations: IDisposable[]; private readonly _searches = new Map(); @@ -134,22 +138,28 @@ class RemoteSearchProvider implements ISearchResultProvider { }); } - handleFindMatch(session: number, dataOrUri: UriComponents | IRawFileMatch2[]): void { + clearCache(cacheKey: string): TPromise { + return this._proxy.$clearCache(this._handle, cacheKey); + } + + handleFindMatch(session: number, dataOrUri: (UriComponents | IRawFileMatch2)[]): void { if (!this._searches.has(session)) { // ignore... return; } const searchOp = this._searches.get(session); - if (Array.isArray(dataOrUri)) { - dataOrUri.forEach(m => { + dataOrUri.forEach(result => { + if ((result).lineMatches) { searchOp.addMatch({ - resource: URI.revive(m.resource), - lineMatches: m.lineMatches + resource: URI.revive((result).resource), + lineMatches: (result).lineMatches }); - }); - } else { - searchOp.addMatch({ resource: URI.revive(dataOrUri) }); - } + } else { + searchOp.addMatch({ + resource: URI.revive(result) + }); + } + }); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 45c911be480..a3e9c819851 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -7,10 +7,12 @@ import * as nls from 'vs/nls'; import URI from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; import * as Objects from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; +import { IStringDictionary } from 'vs/base/common/collections'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -37,10 +39,10 @@ namespace TaskExecutionDTO { task: TaskDTO.from(value.task) }; } - export function to(value: TaskExecutionDTO, workspace: IWorkspaceContextService): TaskExecution { + export function to(value: TaskExecutionDTO, workspace: IWorkspaceContextService, executeOnly: boolean): TaskExecution { return { id: value.id, - task: TaskDTO.to(value.task, workspace) + task: TaskDTO.to(value.task, workspace, executeOnly) }; } } @@ -69,8 +71,15 @@ namespace TaskDefinitionDTO { delete result._key; return result; } - export function to(value: TaskDefinitionDTO): KeyedTaskIdentifier { - return TaskDefinition.createTaskIdentifier(value, console); + export function to(value: TaskDefinitionDTO, executeOnly: boolean): KeyedTaskIdentifier { + let result = TaskDefinition.createTaskIdentifier(value, console); + if (result === void 0 && executeOnly) { + result = { + _key: generateUuid(), + type: '$executeOnly' + }; + } + return result; } } @@ -301,7 +310,7 @@ namespace TaskDTO { return result; } - export function to(task: TaskDTO, workspace: IWorkspaceContextService): Task { + export function to(task: TaskDTO, workspace: IWorkspaceContextService, executeOnly: boolean): Task { if (typeof task.name !== 'string') { return undefined; } @@ -320,7 +329,7 @@ namespace TaskDTO { let source = TaskSourceDTO.to(task.source, workspace); let label = nls.localize('task.label', '{0}: {1}', source.label, task.name); - let definition = TaskDefinitionDTO.to(task.definition); + let definition = TaskDefinitionDTO.to(task.definition, executeOnly); let id = `${task.source.extensionId}.${definition._key}`; let result: ContributedTask = { _id: id, // uuidMap.getUUID(identifier), @@ -386,8 +395,8 @@ export class MainThreadTask implements MainThreadTaskShape { public $registerTaskProvider(handle: number): TPromise { this._taskService.registerTaskProvider(handle, { - provideTasks: () => { - return this._proxy.$provideTasks(handle).then((value) => { + provideTasks: (validTypes: IStringDictionary) => { + return this._proxy.$provideTasks(handle, validTypes).then((value) => { let tasks: Task[] = []; for (let task of value.tasks) { let taskTransfer = task._source as any as ExtensionTaskSourceTransfer; @@ -448,7 +457,7 @@ export class MainThreadTask implements MainThreadTaskShape { reject(new Error('Task not found')); }); } else { - let task = TaskDTO.to(value, this._workspaceContextServer); + let task = TaskDTO.to(value, this._workspaceContextServer, true); this._taskService.run(task); let result: TaskExecutionDTO = { id: task._id, diff --git a/src/vs/workbench/api/electron-browser/mainThreadUrls.ts b/src/vs/workbench/api/electron-browser/mainThreadUrls.ts index 7ba2ed813b1..f85ebfdea27 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadUrls.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadUrls.ts @@ -42,7 +42,7 @@ export class MainThreadUrls implements MainThreadUrlsShape { this.proxy = context.getProxy(ExtHostContext.ExtHostUrls); } - $registerProtocolHandler(handle: number, extensionId: string): TPromise { + $registerUriHandler(handle: number, extensionId: string): TPromise { const handler = new ExtensionUrlHandler(this.proxy, handle, extensionId); const disposable = this.urlService.registerHandler(handler); @@ -52,7 +52,7 @@ export class MainThreadUrls implements MainThreadUrlsShape { return TPromise.as(null); } - $unregisterProtocolHandler(handle: number): TPromise { + $unregisterUriHandler(handle: number): TPromise { const tuple = this.handlers.get(handle); if (!tuple) { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 4c2d53c73a3..8def9ec323a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -117,7 +117,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._webviewService.revealWebview(webview, targetGroup || this._editorGroupService.activeGroup, preserveFocus); } - public async $postMessage(handle: WebviewPanelHandle, message: any): TPromise { + public $postMessage(handle: WebviewPanelHandle, message: any): TPromise { const webview = this.getWebview(handle); const editors = this._editorService.visibleControls .filter(e => e instanceof WebviewEditor) @@ -128,7 +128,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv editor.sendMessage(message); } - return (editors.length > 0); + return TPromise.as(editors.length > 0); } public $registerSerializer(viewType: string): void { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index cea90bb2a5b..eea4190baed 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -335,7 +335,7 @@ export function createApiFactory( return proposedApiFunction(extension, extHostTerminalService.activeTerminal); }, get terminals() { - return proposedApiFunction(extension, extHostTerminalService.terminals); + return extHostTerminalService.terminals; }, showTextDocument(documentOrUri: vscode.TextDocument | vscode.Uri, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): TPromise { let documentPromise: TPromise; @@ -372,9 +372,9 @@ export function createApiFactory( onDidCloseTerminal(listener, thisArg?, disposables?) { return extHostTerminalService.onDidCloseTerminal(listener, thisArg, disposables); }, - onDidOpenTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + onDidOpenTerminal(listener, thisArg?, disposables?) { return extHostTerminalService.onDidOpenTerminal(listener, thisArg, disposables); - }), + }, onDidChangeActiveTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { return extHostTerminalService.onDidChangeActiveTerminal(listener, thisArg, disposables); }), @@ -452,9 +452,9 @@ export function createApiFactory( registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { return extHostDecorations.registerDecorationProvider(provider, extension.id); }), - registerProtocolHandler: proposedApiFunction(extension, (handler: vscode.ProtocolHandler) => { - return extHostUrls.registerProtocolHandler(extension.id, handler); - }), + registerUriHandler(handler: vscode.UriHandler) { + return extHostUrls.registerUriHandler(extension.id, handler); + }, get quickInputBackButton() { return proposedApiFunction(extension, (): vscode.QuickInputButton => { return extHostQuickOpen.backButton; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 21850c7ed3a..c65fd9cf161 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -402,7 +402,7 @@ export interface TransferInputBox extends BaseTransferQuickInput { } export interface MainThreadQuickOpenShape extends IDisposable { - $show(options: IPickOptions): TPromise; + $show(options: IPickOptions): TPromise; $setItems(items: TransferQuickPickItems[]): TPromise; $setError(error: Error): TPromise; $input(options: vscode.InputBoxOptions, validateInput: boolean): TPromise; @@ -453,8 +453,8 @@ export interface ExtHostWebviewsShape { } export interface MainThreadUrlsShape extends IDisposable { - $registerProtocolHandler(handle: number, extensionId: string): TPromise; - $unregisterProtocolHandler(handle: number): TPromise; + $registerUriHandler(handle: number, extensionId: string): TPromise; + $unregisterUriHandler(handle: number): TPromise; } export interface ExtHostUrlsShape { @@ -483,7 +483,8 @@ export interface MainThreadFileSystemShape extends IDisposable { export interface MainThreadSearchShape extends IDisposable { $registerSearchProvider(handle: number, scheme: string): void; $unregisterProvider(handle: number): void; - $handleFindMatch(handle: number, session: number, data: UriComponents | IRawFileMatch2[]): void; + $handleFileMatch(handle: number, session: number, data: UriComponents[]): void; + $handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void; $handleTelemetry(eventName: string, data: any): void; } @@ -681,6 +682,7 @@ export interface ExtHostFileSystemShape { export interface ExtHostSearchShape { $provideFileSearchResults(handle: number, session: number, query: IRawSearchQuery): TPromise; + $clearCache(handle: number, cacheKey: string): TPromise; $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, query: IRawSearchQuery): TPromise; } @@ -876,7 +878,7 @@ export interface ExtHostSCMShape { } export interface ExtHostTaskShape { - $provideTasks(handle: number): TPromise; + $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): TPromise; $onDidStartTask(execution: TaskExecutionDTO): void; $onDidStartTaskProcess(value: TaskProcessStartedDTO): void; $onDidEndTaskProcess(value: TaskProcessEndedDTO): void; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 041ab383c13..192fc8c86da 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -123,7 +123,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } } - public async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + public $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { if (args.kind === 'integrated') { @@ -136,20 +136,31 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { }); } - let t = this._integratedTerminalInstance; + return new TPromise(resolve => { + if (this._integratedTerminalInstance) { + this._integratedTerminalInstance.processId.then(pid => { + resolve(hasChildprocesses(pid)); + }, err => { + resolve(true); + }); + } else { + resolve(true); + } + }).then(needNewTerminal => { - if ((t && hasChildprocesses(await t.processId)) || !t) { - t = this._terminalService.createTerminal(args.title || nls.localize('debug.terminal.title', "debuggee")); - this._integratedTerminalInstance = t; - } - t.show(); + if (needNewTerminal) { + this._integratedTerminalInstance = this._terminalService.createTerminal(args.title || nls.localize('debug.terminal.title', "debuggee")); + } - return new TPromise((resolve, error) => { - setTimeout(_ => { - const command = prepareCommand(args, config); - t.sendText(command, true); - resolve(void 0); - }, 500); + this._integratedTerminalInstance.show(); + + return new TPromise((resolve, error) => { + setTimeout(_ => { + const command = prepareCommand(args, config); + this._integratedTerminalInstance.sendText(command, true); + resolve(void 0); + }, 500); + }); }); } else if (args.kind === 'external') { @@ -415,9 +426,9 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private fireBreakpointChanges(added: vscode.Breakpoint[], removed: vscode.Breakpoint[], changed: vscode.Breakpoint[]) { if (added.length > 0 || removed.length > 0 || changed.length > 0) { this._onDidChangeBreakpoints.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed), - changed: Object.freeze(changed) + added, + removed, + changed, })); } } diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index b008b72b086..eeafdc4a416 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -237,14 +237,14 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG return this._resourceStatesMap.get(handle); } - async $executeResourceCommand(handle: number): TPromise { + $executeResourceCommand(handle: number): TPromise { const command = this._resourceStatesCommandsMap.get(handle); if (!command) { - return; + return TPromise.as(null); } - await this._commands.executeCommand(command.command, ...command.arguments); + return asWinJsPromise(_ => this._commands.executeCommand(command.command, ...command.arguments)); } _takeResourceStateSnapshot(): SCMRawResourceSplice[] { @@ -568,25 +568,25 @@ export class ExtHostSCM implements ExtHostSCMShape { return TPromise.as(null); } - async $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise { + $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise { this.logService.trace('ExtHostSCM#$executeResourceCommand', sourceControlHandle, groupHandle, handle); const sourceControl = this._sourceControls.get(sourceControlHandle); if (!sourceControl) { - return; + return TPromise.as(null); } const group = sourceControl.getResourceGroup(groupHandle); if (!group) { - return; + return TPromise.as(null); } - await group.$executeResourceCommand(handle); + return group.$executeResourceCommand(handle); } - async $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined> { + $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined> { this.logService.trace('ExtHostSCM#$validateInput', sourceControlHandle); const sourceControl = this._sourceControls.get(sourceControlHandle); @@ -599,12 +599,12 @@ export class ExtHostSCM implements ExtHostSCMShape { return TPromise.as(undefined); } - const result = await sourceControl.inputBox.validateInput(value, cursorPosition); + return asWinJsPromise(_ => Promise.resolve(sourceControl.inputBox.validateInput(value, cursorPosition))).then(result => { + if (!result) { + return TPromise.as(undefined); + } - if (!result) { - return TPromise.as(undefined); - } - - return [result.message, result.type]; + return TPromise.as<[string, number]>([result.message, result.type]); + }); } } diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index c4cf892bd17..d35c2401007 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -4,23 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as pfs from 'vs/base/node/pfs'; -import * as extfs from 'vs/base/node/extfs'; import * as path from 'path'; -import * as arrays from 'vs/base/common/arrays'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as glob from 'vs/base/common/glob'; +import * as resources from 'vs/base/common/resources'; import * as strings from 'vs/base/common/strings'; import URI, { UriComponents } from 'vs/base/common/uri'; import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import { IItemAccessor, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; -import { ICachedSearchStats, IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchQuery, ISearchCompleteStats, IRawFileMatch2 } from 'vs/platform/search/common/search'; +import * as extfs from 'vs/base/node/extfs'; +import * as pfs from 'vs/base/node/pfs'; +import { IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery } from 'vs/platform/search/common/search'; import * as vscode from 'vscode'; import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { joinPath } from 'vs/base/common/resources'; - -type OneOrMore = T | T[]; export interface ISchemeTransformer { transformOutgoing(scheme: string): string; @@ -36,9 +32,7 @@ export class ExtHostSearch implements ExtHostSearchShape { constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs, private _pfs = pfs) { this._proxy = mainContext.getProxy(MainContext.MainThreadSearch); - this._fileSearchManager = new FileSearchManager( - (eventName: string, data: any) => this._proxy.$handleTelemetry(eventName, data), - this._pfs); + this._fileSearchManager = new FileSearchManager(this._pfs); } private _transformScheme(scheme: string): string { @@ -71,16 +65,20 @@ export class ExtHostSearch implements ExtHostSearchShape { null, null, progress => { - if (Array.isArray(progress)) { - progress.forEach(p => { - this._proxy.$handleFindMatch(handle, session, p.resource); - }); - } else { - this._proxy.$handleFindMatch(handle, session, progress.resource); - } + this._proxy.$handleFileMatch(handle, session, progress.map(p => p.resource)); }); } + $clearCache(handle: number, cacheKey: string): TPromise { + const provider = this._searchProvider.get(handle); + if (!provider.clearCache) { + return TPromise.as(undefined); + } + + return TPromise.as( + this._fileSearchManager.clearCache(cacheKey, provider)); + } + $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, rawQuery: IRawSearchQuery): TPromise { const provider = this._searchProvider.get(handle); if (!provider.provideTextSearchResults) { @@ -93,7 +91,7 @@ export class ExtHostSearch implements ExtHostSearchShape { null, null, progress => { - this._proxy.$handleFindMatch(handle, session, progress); + this._proxy.$handleTextMatch(handle, session, progress); }); } } @@ -132,29 +130,28 @@ function reviveFolderQuery(rawFolderQuery: IFolderQuery): IFolder } class TextSearchResultsCollector { - private _batchedCollector: BatchedCollector; + private _batchedCollector: BatchedCollector; private _currentFolderIdx: number; - private _currentRelativePath: string; - private _currentFileMatch: IRawFileMatch2; + private _currentUri: URI; + private _currentFileMatch: IFileMatch; - constructor(private folderQueries: IFolderQuery[], private _onResult: (result: IRawFileMatch2[]) => void) { - this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); + constructor(private _onResult: (result: IFileMatch[]) => void) { + this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); } add(data: vscode.TextSearchResult, folderIdx: number): void { // Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector. // This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search // providers that send results in random order. We could do this step afterwards instead. - if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || this._currentRelativePath !== data.path)) { + if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || resources.isEqual(this._currentUri, data.uri))) { this.pushToCollector(); this._currentFileMatch = null; } if (!this._currentFileMatch) { - const resource = joinPath(this.folderQueries[folderIdx].folder, data.path); this._currentFileMatch = { - resource, + resource: data.uri, lineMatches: [] }; } @@ -180,8 +177,8 @@ class TextSearchResultsCollector { this._batchedCollector.flush(); } - private sendItems(items: IRawFileMatch2 | IRawFileMatch2[]): void { - this._onResult(Array.isArray(items) ? items : [items]); + private sendItems(items: IFileMatch[]): void { + this._onResult(items); } } @@ -202,7 +199,7 @@ class BatchedCollector { private batchSize = 0; private timeoutHandle: number; - constructor(private maxBatchSize: number, private cb: (items: T | T[]) => void) { + constructor(private maxBatchSize: number, private cb: (items: T[]) => void) { } addItem(item: T, size: number): void { @@ -210,11 +207,7 @@ class BatchedCollector { return; } - if (this.maxBatchSize > 0) { - this.addItemToBatch(item, size); - } else { - this.cb(item); - } + this.addItemToBatch(item, size); } addItems(items: T[], size: number): void { @@ -390,11 +383,11 @@ class TextSearchEngine { this.activeCancellationTokens = new Set(); } - public search(): PPromise<{ limitHit: boolean }, IRawFileMatch2[]> { + public search(): PPromise<{ limitHit: boolean }, IFileMatch[]> { const folderQueries = this.config.folderQueries; - return new PPromise<{ limitHit: boolean }, IRawFileMatch2[]>((resolve, reject, _onResult) => { - this.collector = new TextSearchResultsCollector(this.config.folderQueries, _onResult); + return new PPromise<{ limitHit: boolean }, IFileMatch[]>((resolve, reject, _onResult) => { + this.collector = new TextSearchResultsCollector(_onResult); const onResult = (match: vscode.TextSearchResult, folderIdx: number) => { if (this.isCanceled) { @@ -437,11 +430,12 @@ class TextSearchEngine { const progress = { report: (result: vscode.TextSearchResult) => { const siblingFn = folderQuery.folder.scheme === 'file' && (() => { - return this.readdir(path.dirname(path.join(folderQuery.folder.fsPath, result.path))); + return this.readdir(path.dirname(result.uri.fsPath)); }); + const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath); testingPs.push( - queryTester.includedInQuery(result.path, path.basename(result.path), siblingFn) + queryTester.includedInQuery(relativePath, path.basename(relativePath), siblingFn) .then(included => { if (included) { onResult(result); @@ -495,7 +489,8 @@ class TextSearchEngine { useIgnoreFiles: !this.config.disregardIgnoreFiles, followSymlinks: !this.config.ignoreSymlinks, encoding: this.config.fileEncoding, - maxFileSize: this.config.maxFileSize + maxFileSize: this.config.maxFileSize, + maxResults: this.config.maxResults }; } } @@ -515,16 +510,12 @@ class FileSearchEngine { private includePattern: glob.ParsedExpression; private maxResults: number; private exists: boolean; - // private maxFilesize: number; private isLimitHit: boolean; private resultCount: number; private isCanceled: boolean; private activeCancellationTokens: Set; - // private filesWalked: number; - // private directoriesWalked: number; - private globalExcludePattern: glob.ParsedExpression; constructor(private config: ISearchQuery, private provider: vscode.SearchProvider, private _pfs: typeof pfs) { @@ -532,14 +523,10 @@ class FileSearchEngine { this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || null; this.exists = config.exists; - // this.maxFilesize = config.maxFileSize || null; this.resultCount = 0; this.isLimitHit = false; this.activeCancellationTokens = new Set(); - // this.filesWalked = 0; - // this.directoriesWalked = 0; - if (this.filePattern) { this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase(); } @@ -553,87 +540,68 @@ class FileSearchEngine { this.activeCancellationTokens = new Set(); } - public search(): PPromise<{ isLimitHit: boolean }, IInternalFileMatch> { + public search(): PPromise { const folderQueries = this.config.folderQueries; - return new PPromise<{ isLimitHit: boolean }, IInternalFileMatch>((resolve, reject, _onResult) => { + return new PPromise((resolve, reject, _onResult) => { const onResult = (match: IInternalFileMatch) => { this.resultCount++; _onResult(match); }; // Support that the file pattern is a full path to a file that exists - this.checkFilePatternAbsoluteMatch().then(({ exists, size }) => { - if (this.isCanceled) { - return resolve({ isLimitHit: this.isLimitHit }); - } + if (this.isCanceled) { + return resolve({ limitHit: this.isLimitHit, cacheKeys: [] }); + } - // Report result from file pattern if matching - if (exists) { - onResult({ - base: URI.file(this.filePattern), - basename: path.basename(this.filePattern), - size + // For each extra file + if (this.config.extraFileResources) { + this.config.extraFileResources + .forEach(extraFile => { + const extraFileStr = extraFile.toString(); // ? + const basename = path.basename(extraFileStr); + if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) { + return; // excluded + } + + // File: Check for match on file pattern and include pattern + this.matchFile(onResult, { base: extraFile, basename }); }); + } - // Optimization: a match on an absolute path is a good result and we do not - // continue walking the entire root paths array for other matches because - // it is very unlikely that another file would match on the full absolute path - return resolve({ isLimitHit: this.isLimitHit }); - } + // For each root folder + PPromise.join(folderQueries.map(fq => { + return this.searchInFolder(fq).then(null, null, onResult); + })).then(cacheKeys => { + resolve({ limitHit: this.isLimitHit, cacheKeys }); + }, (errs: Error[]) => { + const errMsg = errs + .map(err => toErrorMessage(err)) + .filter(msg => !!msg)[0]; - // For each extra file - if (this.config.extraFileResources) { - this.config.extraFileResources - .forEach(extraFile => { - const extraFileStr = extraFile.toString(); // ? - const basename = path.basename(extraFileStr); - if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) { - return; // excluded - } - - // File: Check for match on file pattern and include pattern - this.matchFile(onResult, { base: extraFile, basename }); - }); - } - - // For each root folder - PPromise.join(folderQueries.map(fq => { - return this.searchInFolder(fq).then(null, null, onResult); - })).then(() => { - resolve({ isLimitHit: this.isLimitHit }); - }, (errs: Error[]) => { - const errMsg = errs - .map(err => toErrorMessage(err)) - .filter(msg => !!msg)[0]; - - reject(new Error(errMsg)); - }); + reject(new Error(errMsg)); }); }); } - private searchInFolder(fq: IFolderQuery): PPromise { + private searchInFolder(fq: IFolderQuery): PPromise { let cancellation = new CancellationTokenSource(); return new PPromise((resolve, reject, onResult) => { const options = this.getSearchOptionsForFolder(fq); - let filePatternSeen = false; const tree = this.initDirectoryTree(); const queryTester = new QueryGlobTester(this.config, fq); const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses(); - const onProviderResult = (relativePath: string) => { + const onProviderResult = (result: URI) => { if (this.isCanceled) { return; } - if (noSiblingsClauses) { - if (relativePath === this.filePattern) { - filePatternSeen = true; - } + const relativePath = path.relative(fq.folder.fsPath, result.fsPath); - const basename = path.basename(relativePath); + if (noSiblingsClauses) { + const basename = path.basename(result.fsPath); this.matchFile(onResult, { base: fq.folder, relativePath, basename }); return; @@ -643,10 +611,21 @@ class FileSearchEngine { this.addDirectoryEntries(tree, fq.folder, relativePath, onResult); }; - new TPromise(resolve => process.nextTick(resolve)) + let folderCacheKey: string; + new TPromise(_resolve => process.nextTick(_resolve)) .then(() => { this.activeCancellationTokens.add(cancellation); - return this.provider.provideFileSearchResults(options, { report: onProviderResult }, cancellation.token); + + folderCacheKey = this.config.cacheKey && (this.config.cacheKey + '_' + fq.folder.fsPath); + + return this.provider.provideFileSearchResults( + { + pattern: this.config.filePattern || '', + cacheKey: folderCacheKey + }, + options, + { report: onProviderResult }, + cancellation.token); }) .then(() => { this.activeCancellationTokens.delete(cancellation); @@ -655,18 +634,16 @@ class FileSearchEngine { } if (noSiblingsClauses && this.isLimitHit) { - if (!filePatternSeen) { - // If the limit was hit, check whether filePattern is an exact relative match because it must be included - return this.checkFilePatternRelativeMatch(fq.folder).then(({ exists, size }) => { - if (exists) { - onResult({ - base: fq.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - }); - } - }); - } + // If the limit was hit, check whether filePattern is an exact relative match because it must be included + return this.checkFilePatternRelativeMatch(fq.folder).then(({ exists, size }) => { + if (exists) { + onResult({ + base: fq.folder, + relativePath: this.filePattern, + basename: path.basename(this.filePattern), + }); + } + }); } this.matchDirectoryTree(tree, queryTester, onResult); @@ -674,7 +651,7 @@ class FileSearchEngine { }).then( () => { cancellation.dispose(); - resolve(undefined); + resolve(folderCacheKey); }, err => { cancellation.dispose(); @@ -692,7 +669,8 @@ class FileSearchEngine { excludes, includes, useIgnoreFiles: !this.config.disregardIgnoreFiles, - followSymlinks: !this.config.ignoreSymlinks + followSymlinks: !this.config.ignoreSymlinks, + maxResults: this.config.maxResults }; } @@ -734,7 +712,6 @@ class FileSearchEngine { const self = this; const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { - // self.directoriesWalked++; for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -751,7 +728,6 @@ class FileSearchEngine { if (sub) { matchDirectory(sub); } else { - // self.filesWalked++; if (relativePath === filePattern) { continue; // ignore file if its path matches with the file pattern because that is already matched above } @@ -767,44 +743,6 @@ class FileSearchEngine { matchDirectory(rootEntries); } - public getStats(): any { - return null; - // return { - // fromCache: false, - // traversal: Traversal[this.traversal], - // errors: this.errors, - // fileWalkStartTime: this.fileWalkStartTime, - // fileWalkResultTime: Date.now(), - // directoriesWalked: this.directoriesWalked, - // filesWalked: this.filesWalked, - // resultCount: this.resultCount, - // cmdForkResultTime: this.cmdForkResultTime, - // cmdResultCount: this.cmdResultCount - // }; - } - - /** - * Return whether the file pattern is an absolute path to a file that exists. - * TODO@roblou should use FS provider? - */ - private checkFilePatternAbsoluteMatch(): TPromise<{ exists: boolean, size?: number }> { - if (!this.filePattern || !path.isAbsolute(this.filePattern)) { - return TPromise.wrap({ exists: false }); - } - - return this._pfs.stat(this.filePattern) - .then(stat => { - return { - exists: !stat.isDirectory(), - size: stat.size - }; - }, err => { - return { - exists: false - }; - }); - } - private checkFilePatternRelativeMatch(base: URI): TPromise<{ exists: boolean, size?: number }> { if (!this.filePattern || path.isAbsolute(this.filePattern) || base.scheme !== 'file') { return TPromise.wrap({ exists: false }); @@ -851,263 +789,69 @@ class FileSearchEngine { } } +interface IInternalSearchComplete { + limitHit: boolean; + cacheKeys: string[]; +} + class FileSearchManager { private static readonly BATCH_SIZE = 512; - private caches: { [cacheKey: string]: Cache; } = Object.create(null); + private readonly expandedCacheKeys = new Map(); - constructor(private telemetryCallback: (eventName: string, data: any) => void, private _pfs: typeof pfs) { } + constructor(private _pfs: typeof pfs) { } - public fileSearch(config: ISearchQuery, provider: vscode.SearchProvider): PPromise> { - if (config.sortByScore) { - let sortedSearch = this.trySortedSearchFromCache(config); - if (!sortedSearch) { - const engineConfig = config.maxResults ? - { - ...config, - ...{ maxResults: null } - } : - config; - - const engine = new FileSearchEngine(engineConfig, provider, this._pfs); - sortedSearch = this.doSortedSearch(engine, provider, config); - } - - return new PPromise>((c, e, p) => { - process.nextTick(() => { // allow caller to register progress callback first - sortedSearch.then(([result, rawMatches]) => { - const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch)); - this.sendProgress(serializedMatches, p, FileSearchManager.BATCH_SIZE); - c(result); - }, e, p); - }); - }, () => { - sortedSearch.cancel(); - }); - } - - let searchPromise: PPromise>; - return new PPromise>((c, e, p) => { + fileSearch(config: ISearchQuery, provider: vscode.SearchProvider): PPromise { + let searchP: PPromise; + return new PPromise((c, e, p) => { const engine = new FileSearchEngine(config, provider, this._pfs); - searchPromise = this.doSearch(engine, provider, FileSearchManager.BATCH_SIZE) - .then(c, e, progress => { - if (Array.isArray(progress)) { - p(progress.map(m => this.rawMatchToSearchItem(m))); - } else if ((progress).relativePath) { - p(this.rawMatchToSearchItem(progress)); + + searchP = this.doSearch(engine, FileSearchManager.BATCH_SIZE).then( + result => { + if (config.cacheKey) { + this.expandedCacheKeys.set(config.cacheKey, result.cacheKeys); } + + c({ + limitHit: result.limitHit + }); + }, + e, + progress => { + p(progress.map(m => this.rawMatchToSearchItem(m))); }); }, () => { - searchPromise.cancel(); + if (searchP) { + searchP.cancel(); + } }); } + clearCache(cacheKey: string, provider: vscode.SearchProvider): void { + if (!this.expandedCacheKeys.has(cacheKey)) { + return; + } + + this.expandedCacheKeys.get(cacheKey).forEach(key => provider.clearCache(key)); + this.expandedCacheKeys.delete(cacheKey); + } + private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch { return { - resource: joinPath(match.base, match.relativePath) + resource: resources.joinPath(match.base, match.relativePath) }; } - private doSortedSearch(engine: FileSearchEngine, provider: vscode.SearchProvider, config: IRawSearchQuery): PPromise<[ISearchCompleteStats, IInternalFileMatch[]]> { - let searchPromise: PPromise>; - let allResultsPromise = new PPromise<[ISearchCompleteStats, IInternalFileMatch[]], OneOrMore>((c, e, p) => { - let results: IInternalFileMatch[] = []; - searchPromise = this.doSearch(engine, provider, -1) - .then(result => { - c([result, results]); - this.telemetryCallback('fileSearch', null); - }, e, progress => { - if (Array.isArray(progress)) { - results = progress; - } else { - p(progress); - } - }); - }, () => { - searchPromise.cancel(); - }); - - let cache: Cache; - if (config.cacheKey) { - cache = this.getOrCreateCache(config.cacheKey); - cache.resultsToSearchCache[config.filePattern] = allResultsPromise; - allResultsPromise.then(null, err => { - delete cache.resultsToSearchCache[config.filePattern]; - }); - allResultsPromise = this.preventCancellation(allResultsPromise); - } - - let chained: TPromise; - return new PPromise<[ISearchCompleteStats, IInternalFileMatch[]]>((c, e, p) => { - chained = allResultsPromise.then(([result, results]) => { - const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); - const unsortedResultTime = Date.now(); - return this.sortResults(config, results, scorerCache) - .then(sortedResults => { - const sortedResultTime = Date.now(); - - c([{ - stats: { - ...result.stats, - ...{ unsortedResultTime, sortedResultTime } - }, - limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults - }, sortedResults]); - }); - }, e, p); - }, () => { - chained.cancel(); - }); - } - - private getOrCreateCache(cacheKey: string): Cache { - const existing = this.caches[cacheKey]; - if (existing) { - return existing; - } - return this.caches[cacheKey] = new Cache(); - } - - private trySortedSearchFromCache(config: IRawSearchQuery): TPromise<[ISearchCompleteStats, IInternalFileMatch[]]> { - const cache = config.cacheKey && this.caches[config.cacheKey]; - if (!cache) { - return undefined; - } - - const cacheLookupStartTime = Date.now(); - const cached = this.getResultsFromCache(cache, config.filePattern); - if (cached) { - let chained: TPromise; - return new TPromise<[ISearchCompleteStats, IInternalFileMatch[]]>((c, e) => { - chained = cached.then(([result, results, cacheStats]) => { - const cacheLookupResultTime = Date.now(); - return this.sortResults(config, results, cache.scorerCache) - .then(sortedResults => { - const sortedResultTime = Date.now(); - - const stats: ICachedSearchStats = { - fromCache: true, - cacheLookupStartTime: cacheLookupStartTime, - cacheFilterStartTime: cacheStats.cacheFilterStartTime, - cacheLookupResultTime: cacheLookupResultTime, - cacheEntryCount: cacheStats.cacheFilterResultCount, - resultCount: results.length - }; - if (config.sortByScore) { - stats.unsortedResultTime = cacheLookupResultTime; - stats.sortedResultTime = sortedResultTime; - } - if (!cacheStats.cacheWasResolved) { - stats.joined = result.stats; - } - c([ - { - limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults, - stats: stats - }, - sortedResults - ]); - }); - }, e); - }, () => { - chained.cancel(); - }); - } - return undefined; - } - - private sortResults(config: IRawSearchQuery, results: IInternalFileMatch[], scorerCache: ScorerCache): TPromise { - // we use the same compare function that is used later when showing the results using fuzzy scoring - // this is very important because we are also limiting the number of results by config.maxResults - // and as such we want the top items to be included in this result set if the number of items - // exceeds config.maxResults. - const query = prepareQuery(config.filePattern); - const compare = (matchA: IInternalFileMatch, matchB: IInternalFileMatch) => compareItemsByScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache); - - return arrays.topAsync(results, compare, config.maxResults, 10000); - } - - private sendProgress(results: IFileMatch[], progressCb: (batch: IFileMatch[]) => void, batchSize: number) { - if (batchSize && batchSize > 0) { - for (let i = 0; i < results.length; i += batchSize) { - progressCb(results.slice(i, i + batchSize)); - } - } else { - progressCb(results); - } - } - - private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISearchCompleteStats, IInternalFileMatch[], CacheStats]> { - if (path.isAbsolute(searchValue)) { - return null; // bypass cache if user looks up an absolute path where matching goes directly on disk - } - - // Find cache entries by prefix of search value - const hasPathSep = searchValue.indexOf(path.sep) >= 0; - let cached: PPromise<[ISearchCompleteStats, IInternalFileMatch[]], OneOrMore>; - let wasResolved: boolean; - for (let previousSearch in cache.resultsToSearchCache) { - - // If we narrow down, we might be able to reuse the cached results - if (strings.startsWith(searchValue, previousSearch)) { - if (hasPathSep && previousSearch.indexOf(path.sep) < 0) { - continue; // since a path character widens the search for potential more matches, require it in previous search too - } - - const c = cache.resultsToSearchCache[previousSearch]; - c.then(() => { wasResolved = false; }); - wasResolved = true; - cached = this.preventCancellation(c); - break; - } - } - - if (!cached) { - return null; - } - - return new PPromise<[ISearchCompleteStats, IInternalFileMatch[], CacheStats]>((c, e, p) => { - cached.then(([complete, cachedEntries]) => { - const cacheFilterStartTime = Date.now(); - - // Pattern match on results - let results: IInternalFileMatch[] = []; - const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); - for (let i = 0; i < cachedEntries.length; i++) { - let entry = cachedEntries[i]; - - // Check if this entry is a match for the search value - if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { - continue; - } - - results.push(entry); - } - - c([complete, results, { - cacheWasResolved: wasResolved, - cacheFilterStartTime: cacheFilterStartTime, - cacheFilterResultCount: cachedEntries.length - }]); - }, e, p); - }, () => { - cached.cancel(); - }); - } - - private doSearch(engine: FileSearchEngine, provider: vscode.SearchProvider, batchSize?: number): PPromise> { - return new PPromise>((c, e, p) => { + private doSearch(engine: FileSearchEngine, batchSize: number): PPromise { + return new PPromise((c, e, p) => { let batch: IInternalFileMatch[] = []; engine.search().then(result => { if (batch.length) { p(batch); } - c({ - limitHit: result.isLimitHit, - stats: engine.getStats() // TODO@roblou - }); + c(result); }, error => { if (batch.length) { p(batch); @@ -1116,14 +860,10 @@ class FileSearchManager { e(error); }, match => { if (match) { - if (batchSize) { - batch.push(match); - if (batchSize > 0 && batch.length >= batchSize) { - p(batch); - batch = []; - } - } else { - p(match); + batch.push(match); + if (batchSize > 0 && batch.length >= batchSize) { + p(batch); + batch = []; } } }); @@ -1131,48 +871,4 @@ class FileSearchManager { engine.cancel(); }); } - - public clearCache(cacheKey: string): TPromise { - delete this.caches[cacheKey]; - return TPromise.as(undefined); - } - - private preventCancellation(promise: PPromise): PPromise { - return new PPromise((c, e, p) => { - // Allow for piled up cancellations to come through first. - process.nextTick(() => { - promise.then(c, e, p); - }); - }, () => { - // Do not propagate. - }); - } -} - -class Cache { - - public resultsToSearchCache: { [searchValue: string]: PPromise<[ISearchCompleteStats, IInternalFileMatch[]], OneOrMore>; } = Object.create(null); - - public scorerCache: ScorerCache = Object.create(null); -} - -const FileMatchItemAccessor = new class implements IItemAccessor { - - public getItemLabel(match: IInternalFileMatch): string { - return match.basename; // e.g. myFile.txt - } - - public getItemDescription(match: IInternalFileMatch): string { - return match.relativePath.substr(0, match.relativePath.length - match.basename.length - 1); // e.g. some/path/to/file - } - - public getItemPath(match: IInternalFileMatch): string { - return match.relativePath; // e.g. some/path/to/file/myFile.txt - } -}; - -interface CacheStats { - cacheWasResolved: boolean; - cacheFilterStartTime: number; - cacheFilterResultCount: number; } diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 386a0ed6580..c8a40bb16a8 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -860,15 +860,23 @@ export class ExtHostTask implements ExtHostTaskShape { } } - public $provideTasks(handle: number): TPromise { + public $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): TPromise { let handler = this._handlers.get(handle); if (!handler) { return TPromise.wrapError(new Error('no handler found')); } return asWinJsPromise(token => handler.provider.provideTasks(token)).then(value => { + let sanitized: vscode.Task[] = []; + for (let task of value) { + if (task.definition && validTypes[task.definition.type] === true) { + sanitized.push(task); + } else { + console.error(`Dropping task [${task.source}, ${task.name}]. Its type is not known to the system.`); + } + } let workspaceFolders = this._workspaceService.getWorkspaceFolders(); return { - tasks: Tasks.from(value, workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0] : undefined, handler.extension), + tasks: Tasks.from(sanitized, workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0] : undefined, handler.extension), extension: handler.extension }; }); diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 96b275af75f..bf942604411 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -512,6 +512,7 @@ export namespace Suggest { result.documentation = htmlContent.isMarkdownString(suggestion.documentation) ? MarkdownString.to(suggestion.documentation) : suggestion.documentation; result.sortText = suggestion.sortText; result.filterText = suggestion.filterText; + result.preselect = suggestion.preselect; // 'overwrite[Before|After]'-logic let overwriteBefore = (typeof suggestion.overwriteBefore === 'number') ? suggestion.overwriteBefore : 0; diff --git a/src/vs/workbench/api/node/extHostUrls.ts b/src/vs/workbench/api/node/extHostUrls.ts index 534dfb4f02a..06f5b72ca62 100644 --- a/src/vs/workbench/api/node/extHostUrls.ts +++ b/src/vs/workbench/api/node/extHostUrls.ts @@ -8,6 +8,8 @@ import { MainContext, IMainContext, ExtHostUrlsShape, MainThreadUrlsShape } from import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { toDisposable } from 'vs/base/common/lifecycle'; +import { asWinJsPromise } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class ExtHostUrls implements ExtHostUrlsShape { @@ -15,7 +17,7 @@ export class ExtHostUrls implements ExtHostUrlsShape { private readonly _proxy: MainThreadUrlsShape; private handles = new Set(); - private handlers = new Map(); + private handlers = new Map(); constructor( mainContext: IMainContext @@ -23,7 +25,7 @@ export class ExtHostUrls implements ExtHostUrlsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadUrls); } - registerProtocolHandler(extensionId: string, handler: vscode.ProtocolHandler): vscode.Disposable { + registerUriHandler(extensionId: string, handler: vscode.UriHandler): vscode.Disposable { if (this.handles.has(extensionId)) { throw new Error(`Protocol handler already registered for extension ${extensionId}`); } @@ -31,12 +33,12 @@ export class ExtHostUrls implements ExtHostUrlsShape { const handle = ExtHostUrls.HandlePool++; this.handles.add(extensionId); this.handlers.set(handle, handler); - this._proxy.$registerProtocolHandler(handle, extensionId); + this._proxy.$registerUriHandler(handle, extensionId); return toDisposable(() => { this.handles.delete(extensionId); this.handlers.delete(handle); - this._proxy.$unregisterProtocolHandler(handle); + this._proxy.$unregisterUriHandler(handle); }); } @@ -47,7 +49,9 @@ export class ExtHostUrls implements ExtHostUrlsShape { return TPromise.as(null); } - handler.handleUri(URI.revive(uri)); + asWinJsPromise(_ => handler.handleUri(URI.revive(uri))) + .done(null, onUnexpectedError); + return TPromise.as(null); } } \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 89dcbf56605..2afd565ed8a 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -329,8 +329,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { // Events this._onDidChangeWorkspace.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed) + added, + removed, })); } @@ -386,12 +386,12 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { const queryOptions: IQueryOptions = { ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined, disregardIgnoreFiles: typeof options.useIgnoreFiles === 'boolean' ? !options.useIgnoreFiles : undefined, - disregardExcludeSettings: options.excludes === null, + disregardExcludeSettings: options.exclude === null, fileEncoding: options.encoding, maxResults: options.maxResults, - includePattern: options.includes && options.includes.map(include => globPatternToString(include)).join(', '), - excludePattern: options.excludes && options.excludes.map(exclude => globPatternToString(exclude)).join(', ') + includePattern: options.include && globPatternToString(options.include), + excludePattern: options.exclude && globPatternToString(options.exclude) }; this._activeSearchCallbacks[requestId] = p => { @@ -399,7 +399,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { lineMatch.offsetAndLengths.forEach(offsetAndLength => { const range = new Range(lineMatch.lineNumber, offsetAndLength[0], lineMatch.lineNumber, offsetAndLength[0] + offsetAndLength[1]); callback({ - path: URI.revive(p.resource).fsPath, + uri: URI.revive(p.resource), preview: { text: lineMatch.preview, match: range }, range }); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 0db9f7aae65..3f6da3e94dc 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -341,15 +341,17 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe // Files else { - // Name - classes.push(`${name}-name-file-icon`); + // Name & Extension(s) + if (name) { + classes.push(`${name}-name-file-icon`); - // Extension(s) - const dotSegments = name.split('.'); - for (let i = 1; i < dotSegments.length; i++) { - classes.push(`${dotSegments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one + const dotSegments = name.split('.'); + for (let i = 1; i < dotSegments.length; i++) { + classes.push(`${dotSegments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one + } + + classes.push(`ext-file-icon`); // extra segment to increase file-ext score } - classes.push(`ext-file-icon`); // extra segment to increase file-ext score // Configured Language let configuredLangId = getConfiguredLangId(modelService, resource); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 6979382b238..01fe21c6a4c 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -219,7 +219,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } @memoize - private get partLayoutInfo() { + public get partLayoutInfo() { return { titlebar: { height: TITLE_BAR_HEIGHT diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts index d06fa2b65a6..6d6647e8c5a 100644 --- a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts @@ -12,7 +12,7 @@ import * as dom from 'vs/base/browser/dom'; import { Builder, $ } from 'vs/base/browser/builder'; import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose, IDisposable, empty, toDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -130,7 +130,7 @@ export class ActivityActionItem extends BaseActionItem { protected options: IActivityActionItemOptions; private $badgeContent: Builder; - private badgeDisposable: IDisposable = empty; + private badgeDisposable: IDisposable = Disposable.None; private mouseUpTimeout: number; constructor( @@ -230,7 +230,7 @@ export class ActivityActionItem extends BaseActionItem { const clazz = action.getClass(); this.badgeDisposable.dispose(); - this.badgeDisposable = empty; + this.badgeDisposable = Disposable.None; this.$badgeContent.empty(); this.$badge.hide(); diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index aa9d7062c83..3b662fff332 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -73,7 +73,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { return super.setInput(input, options, token).then(() => { - return input.resolve(true).then(model => { + return input.resolve().then(model => { // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index f633777987f..20167466e78 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -37,7 +37,7 @@ import { ShowEditorsInActiveGroupAction, MoveEditorToLastGroupAction, OpenFirstEditorInGroup, MoveGroupUpAction, MoveGroupDownAction, FocusLastGroupAction, SplitEditorLeftAction, SplitEditorRightAction, SplitEditorUpAction, SplitEditorDownAction, MoveEditorToLeftGroupAction, MoveEditorToRightGroupAction, MoveEditorToAboveGroupAction, MoveEditorToBelowGroupAction, CloseAllEditorGroupsAction, JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, - EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoColumnsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, + EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -368,7 +368,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutThreeColum registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoRowsAction, EditorLayoutTwoRowsAction.ID, EditorLayoutTwoRowsAction.LABEL), 'View: Two Rows Editor Layout', category); registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutThreeRowsAction, EditorLayoutThreeRowsAction.ID, EditorLayoutThreeRowsAction.LABEL), 'View: Three Rows Editor Layout', category); registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoByTwoGridAction, EditorLayoutTwoByTwoGridAction.ID, EditorLayoutTwoByTwoGridAction.LABEL), 'View: Grid Editor Layout (2x2)', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoColumnsRightAction, EditorLayoutTwoColumnsRightAction.ID, EditorLayoutTwoColumnsRightAction.LABEL), 'View: Two Columns Right Editor Layout', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoRowsRightAction, EditorLayoutTwoRowsRightAction.ID, EditorLayoutTwoRowsRightAction.LABEL), 'View: Two Rows Right Editor Layout', category); registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoColumnsBottomAction.ID, EditorLayoutTwoColumnsBottomAction.LABEL), 'View: Two Columns Bottom Editor Layout', category); // Register Editor Picker Actions including quick navigate support diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 1e4336156a6..5e63f57bfad 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -159,4 +159,4 @@ export interface EditorGroupsServiceImpl extends IEditorGroupsService { * A promise that resolves when groups have been restored. */ readonly whenRestored: TPromise; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index f1af1f2d54b..5c579fa655d 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -1551,10 +1551,10 @@ export class EditorLayoutTwoColumnsBottomAction extends ExecuteCommandAction { } } -export class EditorLayoutTwoColumnsRightAction extends ExecuteCommandAction { +export class EditorLayoutTwoRowsRightAction extends ExecuteCommandAction { - static readonly ID = 'workbench.action.editorLayoutTwoColumnsRight'; - static readonly LABEL = nls.localize('editorLayoutTwoColumnsRight', "Two Columns Right Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutTwoRowsRight'; + static readonly LABEL = nls.localize('editorLayoutTwoRowsRight', "Two Rows Right Editor Layout"); constructor( id: string, diff --git a/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts b/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts new file mode 100644 index 00000000000..d271ac2a475 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorBreadcrumbs.ts @@ -0,0 +1,550 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as dom from 'vs/base/browser/dom'; +import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { compareFileNames } from 'vs/base/common/comparers'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { dirname, isEqual } from 'vs/base/common/resources'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDataSource, IRenderer, ISelectionEvent, ISorter, ITree, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree'; +import 'vs/css!./media/editorbreadcrumbs'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineController, OutlineDataSource, OutlineItemComparator, OutlineRenderer } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IConstructorSignature2, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { FileLabel } from 'vs/workbench/browser/labels'; +import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/editorBreadcrumbsModel'; +import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorBreadcrumbs, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; + +class Item extends BreadcrumbsItem { + + private readonly _disposables: IDisposable[] = []; + + constructor( + readonly element: BreadcrumbElement, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + } + + dispose(): void { + dispose(this._disposables); + } + + equals(other: BreadcrumbsItem): boolean { + if (!(other instanceof Item)) { + return false; + } + if (this.element instanceof FileElement && other.element instanceof FileElement) { + return isEqual(this.element.uri, other.element.uri); + } + if (this.element instanceof TreeElement && other.element instanceof TreeElement) { + return this.element.id === other.element.id; + } + return false; + } + + render(container: HTMLElement): void { + if (this.element instanceof FileElement) { + // file/folder + let label = this._instantiationService.createInstance(FileLabel, container, {}); + label.setFile(this.element.uri, { + hidePath: true, + fileKind: this.element.isFile ? FileKind.FILE : FileKind.FOLDER + }); + this._disposables.push(label); + + } else if (this.element instanceof OutlineGroup) { + // provider + let label = new IconLabel(container); + label.setValue(this.element.provider.displayName); + this._disposables.push(label); + + } else if (this.element instanceof OutlineElement) { + // symbol + let label = new IconLabel(container); + label.setValue(this.element.symbol.name); + this._disposables.push(label); + } + } +} + +export class EditorBreadcrumbs implements IEditorBreadcrumbs { + + static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false); + static CK_BreadcrumbsFocused = new RawContextKey('breadcrumbsFocused', false); + + private readonly _ckBreadcrumbsVisible: IContextKey; + private readonly _ckBreadcrumbsFocused: IContextKey; + + private readonly _cfEnabled: Config; + + private readonly _disposables = new Array(); + private readonly _domNode: HTMLDivElement; + private readonly _widget: BreadcrumbsWidget; + + private _breadcrumbsDisposables = new Array(); + + constructor( + container: HTMLElement, + private readonly _editorGroup: EditorGroupView, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEditorService private readonly _editorService: IEditorService, + @IFileService private readonly _fileService: IFileService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + ) { + this._domNode = document.createElement('div'); + dom.addClasses(this._domNode, 'editor-breadcrumbs', 'show-file-icons'); + dom.append(container, this._domNode); + + this._widget = new BreadcrumbsWidget(this._domNode); + this._widget.onDidSelectItem(this._onDidSelectItem, this, this._disposables); + this._widget.onDidChangeFocus(val => this._ckBreadcrumbsFocused.set(val), undefined, this._disposables); + this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService)); + + this._cfEnabled = Config.create(configurationService, 'breadcrumbs.enabled'); + this._disposables.push(this._cfEnabled.onDidChange(value => { + if (!value) { + this.closeEditor(undefined); + this._editorGroup.relayout(); + } else if (this._editorGroup.activeEditor) { + this.openEditor(this._editorGroup.activeEditor); + this._editorGroup.relayout(); + } + })); + + this._ckBreadcrumbsVisible = EditorBreadcrumbs.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); + this._ckBreadcrumbsFocused = EditorBreadcrumbs.CK_BreadcrumbsFocused.bindTo(this._contextKeyService); + } + + dispose(): void { + dispose(this._disposables); + this._widget.dispose(); + this._ckBreadcrumbsVisible.reset(); + this._cfEnabled.dispose(); + } + + getPreferredHeight(): number { + return this._cfEnabled.value ? 25 : 0; + } + + layout(dim: dom.Dimension): void { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._widget.layout(dim); + } + + setActive(value: boolean): void { + dom.toggleClass(this._domNode, 'active', value); + } + + openEditor(input: EditorInput): void { + if (!this._cfEnabled.value) { + // not enabled -> return early + return; + } + + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + + let uri = input.getResource(); + if (!uri || !this._fileService.canHandleResource(uri)) { + return this.closeEditor(undefined); + } + + dom.toggleClass(this._domNode, 'hidden', false); + this._ckBreadcrumbsVisible.set(true); + + let control = this._editorGroup.activeControl.getControl() as ICodeEditor; + let model = new EditorBreadcrumbsModel(input.getResource(), isCodeEditor(control) ? control : undefined, this._workspaceService); + let listener = model.onDidUpdate(_ => this._widget.setItems(model.getElements().map(element => new Item(element, this._instantiationService)))); + this._widget.setItems(model.getElements().map(element => new Item(element, this._instantiationService))); + + this._breadcrumbsDisposables.push(model, listener); + } + + closeEditor(input: EditorInput): void { + this._ckBreadcrumbsVisible.set(false); + dom.toggleClass(this._domNode, 'hidden', true); + } + + focus(): void { + this._widget.domFocus(); + } + + focusNext(): void { + this._widget.focusNext(); + } + + focusPrev(): void { + this._widget.focusPrev(); + } + + select(): void { + const item = this._widget.getFocused(); + if (item) { + this._widget.setSelected(item); + } + } + + private _onDidSelectItem(event: IBreadcrumbsItemEvent): void { + if (!event.item) { + return; + } + + this._editorGroup.focus(); + this._contextViewService.showContextView({ + getAnchor() { + return event.node; + }, + render: (container: HTMLElement) => { + dom.addClasses(container, 'monaco-breadcrumbs-picker', 'monaco-workbench', 'show-file-icons'); + let color = this._themeService.getTheme().getColor(SIDE_BAR_BACKGROUND); + container.style.borderColor = color.darken(.2).toString(); + container.style.boxShadow = `${color.toString()} 6px 6px 6px -6px;`; + let { element } = event.item as Item; + let ctor: IConstructorSignature2 = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker; + let res = this._instantiationService.createInstance(ctor, container, element); + res.layout({ width: 250, height: 300 }); + res.onDidPickElement(data => { + this._contextViewService.hideContextView(); + if (!data) { + return; + } + if (URI.isUri(data)) { + // open new editor + this._editorService.openEditor({ resource: data }); + + } else if (data instanceof OutlineElement) { + + let resource: URI; + let candidate = data.parent; + while (candidate) { + if (candidate instanceof OutlineModel) { + resource = candidate.textModel.uri; + break; + } + candidate = candidate.parent; + } + + this._editorService.openEditor({ resource, options: { selection: Range.collapseToStart(data.symbol.selectionRange) } }); + + } + }); + return res; + }, + onHide: () => { + this._widget.setSelected(undefined); + // this._widget.setFocused(undefined); + } + }); + } +} + +export abstract class BreadcrumbsPicker { + + readonly focus: dom.IFocusTracker; + + protected readonly _onDidPickElement = new Emitter(); + readonly onDidPickElement: Event = this._onDidPickElement.event; + + protected readonly _disposables = new Array(); + protected readonly _domNode: HTMLDivElement; + protected readonly _tree: WorkbenchTree; + + constructor( + container: HTMLElement, + input: BreadcrumbElement, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IThemeService protected readonly _themeService: IThemeService, + ) { + this._domNode = document.createElement('div'); + this._domNode.style.background = this._themeService.getTheme().getColor(SIDE_BAR_BACKGROUND).toString(); + container.appendChild(this._domNode); + + this._tree = this._instantiationService.createInstance(WorkbenchTree, this._domNode, this._completeTreeConfiguration({ dataSource: undefined }), {}); + debounceEvent(this._tree.onDidChangeSelection, (_last, cur) => cur, 0)(this._onDidChangeSelection, this, this._disposables); + + this.focus = dom.trackFocus(this._domNode); + this.focus.onDidBlur(_ => this._onDidPickElement.fire(undefined), undefined, this._disposables); + + this._tree.setInput(this._getInput(input)); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this.focus.dispose(); + } + + layout(dim: dom.Dimension) { + this._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + this._tree.layout(dim.height, dim.width); + } + + protected abstract _getInput(input: BreadcrumbElement): any; + protected abstract _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration; + protected abstract _onDidChangeSelection(e: any): void; +} + +export class FileDataSource implements IDataSource { + + private readonly _parents = new WeakMap(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { } + + getId(tree: ITree, element: IFileStat | URI): string { + return URI.isUri(element) ? element.toString() : element.resource.toString(); + } + + hasChildren(tree: ITree, element: IFileStat | URI): boolean { + return URI.isUri(element) || element.isDirectory; + } + + getChildren(tree: ITree, element: IFileStat | URI): TPromise { + return this._fileService.resolveFile( + URI.isUri(element) ? element : element.resource + ).then(stat => { + for (const child of stat.children) { + this._parents.set(child, stat); + } + return stat.children; + }); + } + + getParent(tree: ITree, element: IFileStat | URI): TPromise { + return TPromise.as(URI.isUri(element) ? undefined : this._parents.get(element)); + } +} + +export class FileRenderer implements IRenderer { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { } + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: any): string { + return 'FileStat'; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + return this._instantiationService.createInstance(FileLabel, container, {}); + } + + renderElement(tree: ITree, element: IFileStat, templateId: string, templateData: FileLabel): void { + templateData.setFile(element.resource, { + hidePath: true, + fileKind: element.isDirectory ? FileKind.FOLDER : FileKind.FILE, + fileDecorations: { colors: true, badges: true } + }); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void { + templateData.dispose(); + } +} + +export class FileSorter implements ISorter { + compare(tree: ITree, a: IFileStat, b: IFileStat): number { + if (a.isDirectory === b.isDirectory) { + // same type -> compare on names + return compareFileNames(a.name, b.name); + } else if (a.isDirectory) { + return -1; + } else { + return 1; + } + } +} + +export class BreadcrumbsFilePicker extends BreadcrumbsPicker { + + + protected _getInput(input: BreadcrumbElement): any { + let { uri } = (input as FileElement); + return dirname(uri); + } + + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { + // todo@joh reuse explorer implementations? + config.dataSource = this._instantiationService.createInstance(FileDataSource); + config.renderer = this._instantiationService.createInstance(FileRenderer); + config.sorter = new FileSorter(); + return config; + } + + protected _onDidChangeSelection(e: ISelectionEvent): void { + let [first] = e.selection; + let stat = first as IFileStat; + if (stat && !stat.isDirectory) { + this._onDidPickElement.fire(stat.resource); + } + } +} + +export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { + + protected _getInput(input: BreadcrumbElement): any { + return (input as TreeElement).parent; + } + + protected _completeTreeConfiguration(config: ITreeConfiguration): ITreeConfiguration { + config.dataSource = this._instantiationService.createInstance(OutlineDataSource); + config.renderer = this._instantiationService.createInstance(OutlineRenderer); + config.controller = this._instantiationService.createInstance(OutlineController, {}); + config.sorter = new OutlineItemComparator(); + return config; + } + + protected _onDidChangeSelection(e: ISelectionEvent): void { + if (e.payload && e.payload.didClickOnTwistie) { + return; + } + let [first] = e.selection; + if (first instanceof OutlineElement) { + this._onDidPickElement.fire(first); + } + } +} + +//#region config + +abstract class Config { + + name: string; + value: T; + onDidChange: Event; + abstract dispose(): void; + + static create(service: IConfigurationService, name: string): Config { + + let value: T = service.getValue(name); + let onDidChange = new Emitter(); + + let listener = service.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(name)) { + value = service.getValue(name); + onDidChange.fire(value); + } + }); + + return { + name, + get value() { return value; }, + onDidChange: onDidChange.event, + dispose(): void { + listener.dispose(); + onDidChange.dispose(); + } + }; + } +} + +Registry.as(Extensions.Configuration).registerConfiguration({ + id: 'breadcrumbs', + title: localize('title', "Breadcrumb Navigation"), + order: 101, + type: 'object', + properties: { + 'breadcrumbs.enabled': { + 'description': localize('enabled', "Enable/disable navigation breadcrumbss"), + 'type': 'boolean', + 'default': false + } + } +}); + +//#endregion + +//#region commands + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focus', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + when: EditorBreadcrumbs.CK_BreadcrumbsVisible, + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.focus(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusNext', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.RightArrow, + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.focusNext(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusPrevious', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.LeftArrow, + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.focusPrev(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectFocused', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Enter, + secondary: [KeyCode.UpArrow, KeyCode.Space], + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.breadcrumbs.select(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectEditor', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Escape, + when: ContextKeyExpr.and(EditorBreadcrumbs.CK_BreadcrumbsVisible, EditorBreadcrumbs.CK_BreadcrumbsFocused), + handler(accessor) { + let groups = accessor.get(IEditorGroupsService); + groups.activeGroup.activeControl.focus(); + } +}); + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/editorBreadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/editorBreadcrumbsModel.ts new file mode 100644 index 00000000000..bb1e22f0e68 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorBreadcrumbsModel.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { equals } from 'vs/base/common/arrays'; +import { TimeoutTimer } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { size } from 'vs/base/common/collections'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import * as paths from 'vs/base/common/paths'; +import { isEqual } from 'vs/base/common/resources'; +import URI from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IPosition } from 'vs/editor/common/core/position'; +import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; + +export class FileElement { + constructor( + readonly uri: URI, + readonly isFile: boolean + ) { } +} + +export type BreadcrumbElement = FileElement | OutlineGroup | OutlineElement; + +export class EditorBreadcrumbsModel { + + private readonly _disposables: IDisposable[] = []; + private readonly _fileElements: FileElement[] = []; + + private _outlineElements: (OutlineGroup | OutlineElement)[] = []; + private _outlineDisposables: IDisposable[] = []; + + private _onDidUpdate = new Emitter(); + readonly onDidUpdate: Event = this._onDidUpdate.event; + + constructor( + private readonly _uri: URI, + private readonly _editor: ICodeEditor | undefined, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, + ) { + this._fileElements = EditorBreadcrumbsModel._getFileElements(this._uri, workspaceService); + this._bindToEditor(); + this._onDidUpdate.fire(this); + } + + dispose(): void { + dispose(this._disposables); + } + + getElements(): ReadonlyArray { + return [].concat(this._fileElements, this._outlineElements); + } + + private static _getFileElements(uri: URI, workspaceService: IWorkspaceContextService): FileElement[] { + let result: FileElement[] = []; + let workspace = workspaceService.getWorkspaceFolder(uri); + let path = uri.path; + while (path !== '/') { + if (workspace && isEqual(workspace.uri, uri)) { + break; + } + result.push(new FileElement(uri, result.length === 0)); + path = paths.dirname(path); + uri = uri.with({ path }); + } + return result.reverse(); + } + + private _bindToEditor(): void { + if (!this._editor) { + return; + } + // update as model changes + this._disposables.push(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); + this._disposables.push(this._editor.onDidChangeModel(_ => this._updateOutline())); + this._disposables.push(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); + this._disposables.push(debounceEvent(this._editor.onDidChangeModelContent, _ => _, 350)(_ => this._updateOutline(true))); + this._updateOutline(); + + // stop when editor dies + this._disposables.push(this._editor.onDidDispose(() => this._outlineDisposables = dispose(this._outlineDisposables))); + } + + private _updateOutline(didChangeContent?: boolean): void { + + this._outlineDisposables = dispose(this._outlineDisposables); + if (!didChangeContent) { + this._updateOutlineElements([]); + } + + const buffer = this._editor.getModel(); + if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { + return; + } + + const source = new CancellationTokenSource(); + const versionIdThen = buffer.getVersionId(); + const timeout = new TimeoutTimer(); + + this._outlineDisposables.push({ + dispose: () => { + source.cancel(); + source.dispose(); + timeout.dispose(); + } + }); + + OutlineModel.create(buffer, source.token).then(model => { + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => { + timeout.cancelAndSet(() => { + if (versionIdThen === buffer.getVersionId()) { + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + } + }, 150); + })); + }).catch(err => { + this._updateOutlineElements([]); + onUnexpectedError(err); + }); + } + + private _getOutlineElements(model: OutlineModel, position: IPosition): (OutlineGroup | OutlineElement)[] { + if (!model) { + return []; + } + let item: OutlineGroup | OutlineElement = model.getItemEnclosingPosition(position); + let chain: (OutlineGroup | OutlineElement)[] = []; + while (item) { + chain.push(item); + let parent = item.parent; + if (parent instanceof OutlineModel) { + break; + } + if (parent instanceof OutlineGroup && size(parent.parent.children) === 1) { + break; + } + item = parent; + } + return chain.reverse(); + } + + private _updateOutlineElements(elements: (OutlineGroup | OutlineElement)[]): void { + if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { + this._outlineElements = elements; + this._onDidUpdate.fire(this); + } + } + + private static _outlineElementEquals(a: OutlineGroup | OutlineElement, b: OutlineGroup | OutlineElement): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.id === b.id; + } + } +} diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index 039e66bd4fd..4d99ce5d284 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -153,9 +153,9 @@ export class EditorControl extends Disposable { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same - const forceOpen = options && options.forceOpen; + const forceReload = options && options.forceReload; const inputMatches = control.input && control.input.matches(editor); - if (inputMatches && !forceOpen) { + if (inputMatches && !forceReload) { // Forward options control.setOptions(options); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 23253584873..a31d41c5797 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -19,7 +19,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Themable, EDITOR_GROUP_HEADER_TABS_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER } from 'vs/workbench/common/theme'; -import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder, IEditorBreadcrumbs } from 'vs/workbench/services/group/common/editorGroupsService'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -46,6 +46,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { EditorBreadcrumbs } from 'vs/workbench/browser/parts/editor/editorBreadcrumbs'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -104,6 +105,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private titleContainer: HTMLElement; private titleAreaControl: TitleControl; + private breadcrumbsContainer: HTMLElement; + private breadcrumbsControl: EditorBreadcrumbs; + private progressBar: ProgressBar; private editorContainer: HTMLElement; @@ -190,6 +194,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control this.createTitleAreaControl(); + // Breadcrumbs container + this.breadcrumbsContainer = document.createElement('div'); + addClass(this.breadcrumbsContainer, 'editor-breadcrumbs'); + this.element.appendChild(this.breadcrumbsContainer); + + // Breadcrumbs control + this.createEditorBreadcrumbs(); + // Editor container this.editorContainer = document.createElement('div'); addClass(this.editorContainer, 'editor-container'); @@ -397,6 +409,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } + private createEditorBreadcrumbs(): void { + + if (this.breadcrumbsControl) { + this.breadcrumbsControl.dispose(); + clearNode(this.breadcrumbsContainer); + } + + this.breadcrumbsControl = this.scopedInstantiationService.createInstance(EditorBreadcrumbs, this.breadcrumbsContainer, this); + } + private restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): TPromise { if (this._group.count === 0) { return TPromise.as(void 0); // nothing to show @@ -593,6 +615,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this._label; } + get breadcrumbs(): IEditorBreadcrumbs { + return this.breadcrumbsControl; + } + get disposed(): boolean { return this._disposed; } @@ -618,6 +644,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update title control this.titleAreaControl.setActive(isActive); + this.breadcrumbsControl.setActive(isActive); + // Update styles this.updateStyles(); @@ -789,6 +817,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Show in title control after editor control because some actions depend on it this.titleAreaControl.openEditor(editor); + this.breadcrumbsControl.openEditor(editor); + return openEditorPromise; } @@ -956,8 +986,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.doCloseInactiveEditor(editor); } - // Forward to title control + // Forward to title control & breadcrumbs this.titleAreaControl.closeEditor(editor); + this.breadcrumbsControl.closeEditor(editor); } private doCloseActiveEditor(focusNext = this.accessor.activeGroup === this, fromError?: boolean): void { @@ -1345,7 +1376,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Forward to controls this.titleAreaControl.layout(new Dimension(this.dimension.width, EDITOR_TITLE_HEIGHT)); - this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - EDITOR_TITLE_HEIGHT)); + this.breadcrumbsControl.layout(new Dimension(this.dimension.width, this.breadcrumbsControl.getPreferredHeight())); + this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - (EDITOR_TITLE_HEIGHT + this.breadcrumbsControl.getPreferredHeight()))); + } + + relayout(): void { + if (this.dimension) { + const { width, height } = this.dimension; + this.layout(width, height); + } } toJSON(): ISerializedEditorGroup { @@ -1365,6 +1404,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleAreaControl.dispose(); + this.breadcrumbsControl.dispose(); + super.dispose(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index e5631a42a10..36d3ffb96b5 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -544,6 +544,14 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Mark group as new active group.setActive(true); + // Maximize the group if it is currently minimized + if (this.gridWidget) { + const viewSize = this.gridWidget.getViewSize2(group); + if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) { + this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + } + } + // Event this._onDidActiveGroupChange.fire(group); } diff --git a/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css b/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css new file mode 100644 index 00000000000..e40ca9ba696 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/editorbreadcrumbs.css @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench>.part.editor>.content .editor-group-container:not(.active) .editor-breadcrumbs { + opacity: .8; +} + + +.monaco-workbench>.part.editor>.content .editor-group-container .editor-breadcrumbs .monaco-breadcrumbs .monaco-breadcrumb-item:nth-child(2) { /*first-child is the style-element*/ + padding-left: 8px; +} + +.monaco-breadcrumbs-picker { + border: 1px #dddddd solid; + box-shadow: #dddddd 6px 6px 6px -6px; +} diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 190eb410696..f59bae6f10b 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -104,7 +104,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { // Set input and resolve return super.setInput(input, options, token).then(() => { - return input.resolve(true).then(resolvedModel => { + return input.resolve().then(resolvedModel => { // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 2154600d6a8..0c0d212617b 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -61,7 +61,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { // Set input and resolve return super.setInput(input, options, token).then(() => { - return input.resolve(true).then((resolvedModel: EditorModel) => { + return input.resolve().then((resolvedModel: EditorModel) => { // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css index 3f1a72cca49..62b8c849063 100644 --- a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css +++ b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css @@ -8,7 +8,7 @@ position: absolute; font-size: 12px; box-sizing: border-box; - padding-left: 30px; + padding-left: 35px; padding-right: 138px; height: 30px; } @@ -25,7 +25,7 @@ flex-shrink: 0; align-items: center; box-sizing: border-box; - padding: 0px 5px; + padding: 0px 8px; position: relative; cursor: default; -webkit-app-region: no-drag; diff --git a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts index 8d877ddba77..e1d7d69ccdd 100644 --- a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts +++ b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts @@ -840,8 +840,8 @@ function layoutMenuRegistration() { MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { group: '2_layouts', command: { - id: 'workbench.action.editorLayoutTwoColumnsRight', - title: nls.localize({ key: 'miTwoColumnsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two C&&olumns Right") + id: 'workbench.action.editorLayoutTwoRowsRight', + title: nls.localize({ key: 'miTwoRowsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two R&&ows Right") }, order: 8 }); diff --git a/src/vs/workbench/browser/parts/menubar/menubarPart.ts b/src/vs/workbench/browser/parts/menubar/menubarPart.ts index af50fa982b4..208b9170360 100644 --- a/src/vs/workbench/browser/parts/menubar/menubarPart.ts +++ b/src/vs/workbench/browser/parts/menubar/menubarPart.ts @@ -74,7 +74,7 @@ export class MenubarPart extends Part { 'Selection': nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"), 'View': nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"), 'Go': nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"), - 'Terminal': nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), + 'Terminal': nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "Ter&&minal"), 'Debug': nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug"), 'Tasks': nls.localize({ key: 'mTasks', comment: ['&& denotes a mnemonic'] }, "&&Tasks"), 'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") @@ -120,33 +120,33 @@ export class MenubarPart extends Part { super(id, { hasTitle: false }, themeService); this.topLevelMenus = { - 'File': this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService), - 'Edit': this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService), - 'Selection': this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService), - 'View': this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService), - 'Go': this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService), - 'Terminal': this.menuService.createMenu(MenuId.MenubarTerminalMenu, this.contextKeyService), - 'Debug': this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService), - 'Tasks': this.menuService.createMenu(MenuId.MenubarTasksMenu, this.contextKeyService), - 'Help': this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService) + 'File': this._register(this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService)), + 'Edit': this._register(this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService)), + 'Selection': this._register(this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService)), + 'View': this._register(this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService)), + 'Go': this._register(this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService)), + 'Terminal': this._register(this.menuService.createMenu(MenuId.MenubarTerminalMenu, this.contextKeyService)), + 'Debug': this._register(this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService)), + 'Tasks': this._register(this.menuService.createMenu(MenuId.MenubarTasksMenu, this.contextKeyService)), + 'Help': this._register(this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService)) }; if (isMacintosh) { - this.topLevelMenus['Window'] = this.menuService.createMenu(MenuId.MenubarWindowMenu, this.contextKeyService); + this.topLevelMenus['Window'] = this._register(this.menuService.createMenu(MenuId.MenubarWindowMenu, this.contextKeyService)); } - this.actionRunner = new ActionRunner(); - this.actionRunner.onDidBeforeRun(() => { + this.actionRunner = this._register(new ActionRunner()); + this._register(this.actionRunner.onDidBeforeRun(() => { if (this.focusedMenu && this.focusedMenu.holder) { this.focusedMenu.holder.hide(); } - }); + })); - this._onVisibilityChange = new Emitter(); + this._onVisibilityChange = this._register(new Emitter()); if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') { for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { - this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar()); + this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar())); } this.setupMenubar(); } @@ -274,21 +274,21 @@ export class MenubarPart extends Part { } private registerListeners(): void { - browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen()); + this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); // Update when config changes - this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); // Listen to update service // this.updateService.onStateChange(() => this.setupMenubar()); // Listen for changes in recently opened menu - this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); }); + this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); // Listen to keybindings change - this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar()); + this._register(this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar())); - ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this); + this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); } private setupMenubar(): void { @@ -486,10 +486,10 @@ export class MenubarPart extends Part { }; this.customMenus[menuIndex].actions = []; - menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions)); + this._register(menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions))); updateActions(menu, this.customMenus[menuIndex].actions); - this.customMenus[menuIndex].titleElement.on(EventType.CLICK, (event) => { + this.customMenus[menuIndex].titleElement.on(EventType.CLICK, () => { if (this._modifierKeyStatus && (this._modifierKeyStatus.shiftKey || this._modifierKeyStatus.ctrlKey)) { return; // supress keyboard shortcuts that shouldn't conflict } @@ -498,21 +498,21 @@ export class MenubarPart extends Part { this.isFocused = !this.isFocused; }); - this.customMenus[menuIndex].titleElement.getHTMLElement().onmouseenter = () => { + this.customMenus[menuIndex].titleElement.on(EventType.MOUSE_ENTER, () => { if (this.isFocused && !this.isCurrentMenu(menuIndex)) { this.toggleCustomMenu(menuIndex); } - }; + }); - this.customMenus[menuIndex].titleElement.getHTMLElement().onmouseleave = () => { + this.customMenus[menuIndex].titleElement.on(EventType.MOUSE_LEAVE, () => { if (!this.isFocused) { this.cleanupCustomMenu(); } - }; + }); - this.customMenus[menuIndex].titleElement.getHTMLElement().onblur = () => { + this.customMenus[menuIndex].titleElement.on(EventType.BLUR, () => { this.cleanupCustomMenu(); - }; + }); } this.container.off(EventType.KEY_DOWN); @@ -662,19 +662,19 @@ export class MenubarPart extends Part { // actionItemProvider: (action) => { return this._getActionItem(action); } }; - let menuWidget = new Menu(menuHolder.getHTMLElement(), customMenu.actions, menuOptions); + let menuWidget = this._register(new Menu(menuHolder.getHTMLElement(), customMenu.actions, menuOptions)); - menuWidget.onDidCancel(() => { + this._register(menuWidget.onDidCancel(() => { this.cleanupCustomMenu(); this.isFocused = false; - }); + })); - menuWidget.onDidBlur(() => { + this._register(menuWidget.onDidBlur(() => { setTimeout(() => { this.cleanupCustomMenu(); this.isFocused = false; }, 100); - }); + })); menuWidget.focus(); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index c6eea59e167..b56ee969d43 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -42,6 +42,7 @@ import URI from 'vs/base/common/uri'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { equals } from 'vs/base/common/arrays'; +import { TimeoutTimer } from 'vs/base/common/async'; const $ = dom.$; @@ -104,7 +105,7 @@ class QuickInput implements IQuickInput { this.onDidHideEmitter, ]; - private busyDelay: TPromise; + private busyDelay: TimeoutTimer; constructor(protected ui: QuickInputUI) { } @@ -215,12 +216,12 @@ class QuickInput implements IQuickInput { this.ui.title.textContent = title; } if (this.busy && !this.busyDelay) { - this.busyDelay = TPromise.timeout(800); - this.busyDelay.then(() => { + this.busyDelay = new TimeoutTimer(); + this.busyDelay.setIfNotSet(() => { if (this.visible) { this.ui.progressBar.infinite(); } - }, () => { /* ignore */ }); + }, 800); } if (!this.busy && this.busyDelay) { this.ui.progressBar.stop(); @@ -890,8 +891,8 @@ export class QuickInputService extends Component implements IQuickInputService { this.updateStyles(); } - pick(picks: TPromise, options: O = {}, token: CancellationToken = CancellationToken.None): TPromise { - return new TPromise((resolve, reject, progress) => { + pick>(picks: TPromise, options: O = {}, token: CancellationToken = CancellationToken.None): TPromise { + return new TPromise((resolve, reject) => { if (token.isCancellationRequested) { resolve(undefined); return; @@ -913,8 +914,8 @@ export class QuickInputService extends Component implements IQuickInputService { }), input.onDidChangeActive(items => { const focused = items[0]; - if (focused) { - progress(focused); + if (focused && options.onDidFocus) { + options.onDidFocus(focused); } }), input.onDidChangeSelection(items => { diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 5bc53c73ece..7cab9eec672 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -34,7 +34,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG } from 'vs/workbench/browser/quickopen'; import * as errors from 'vs/base/common/errors'; -import { IPickOpenEntry, IFilePickOpenEntry, IQuickOpenService, IPickOptions, IShowOptions, IPickOpenItem } from 'vs/platform/quickOpen/common/quickOpen'; +import { IPickOpenEntry, IFilePickOpenEntry, IQuickOpenService, IShowOptions, IPickOpenItem, IStringPickOptions, ITypedPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -73,6 +73,7 @@ interface IInternalPickOptions { ignoreFocusLost?: boolean; quickNavigateConfiguration?: IQuickNavigateConfiguration; onDidType?: (value: string) => any; + onDidFocus?: (item: any) => void; } export class QuickOpenController extends Component implements IQuickOpenService { @@ -145,11 +146,11 @@ export class QuickOpenController extends Component implements IQuickOpenService } } - pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise; - pick(arg1: string[] | TPromise | IPickOpenEntry[] | TPromise, options?: IPickOptions, token?: CancellationToken): TPromise { + pick(picks: TPromise, options?: IStringPickOptions, token?: CancellationToken): TPromise; + pick(picks: TPromise, options?: ITypedPickOptions, token?: CancellationToken): TPromise; + pick(picks: string[], options?: IStringPickOptions, token?: CancellationToken): TPromise; + pick(picks: T[], options?: ITypedPickOptions, token?: CancellationToken): TPromise; + pick(arg1: string[] | TPromise | IPickOpenEntry[] | TPromise, options?: IStringPickOptions | ITypedPickOptions, token?: CancellationToken): TPromise { if (!options) { options = Object.create(null); } @@ -180,13 +181,13 @@ export class QuickOpenController extends Component implements IQuickOpenService this.pickOpenWidget.hide(HideReason.CANCELED); } - return new TPromise((resolve, reject, progress) => { + return new TPromise((resolve, reject) => { function onItem(item: IPickOpenEntry): string | IPickOpenEntry { return item && isAboutStrings ? item.label : item; } - this.doPick(entryPromise, options, token).then(item => resolve(onItem(item)), err => reject(err), item => progress(onItem(item))); + this.doPick(entryPromise, options, token).then(item => resolve(onItem(item)), err => reject(err)); }); } @@ -248,7 +249,7 @@ export class QuickOpenController extends Component implements IQuickOpenService this.pickOpenWidget.layout(this.layoutDimensions); } - return new TPromise((complete, error, progress) => { + return new TPromise((complete, error) => { // Detect cancellation while pick promise is loading this.pickOpenWidget.setCallbacks({ @@ -279,7 +280,14 @@ export class QuickOpenController extends Component implements IQuickOpenService // Model const model = new QuickOpenModel([], new PickOpenActionProvider()); - const entries = picks.map((e, index) => this.instantiationService.createInstance(PickOpenEntry, e, index, () => progress(e), () => this.pickOpenWidget.refresh())); + const entries = picks.map((e, index) => { + const onPreview = () => { + if (options.onDidFocus) { + options.onDidFocus(e); + } + }; + return this.instantiationService.createInstance(PickOpenEntry, e, index, onPreview, () => this.pickOpenWidget.refresh()); + }); if (picks.length === 0) { entries.push(this.instantiationService.createInstance(PickOpenEntry, { label: nls.localize('emptyPicks', "There are no entries to pick from") }, 0, null, null)); } diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 770803a9a34..a8330e6fc88 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -45,7 +45,7 @@ position: absolute; top: 0; width: 100%; - height: 1px; + height: 20%; } .monaco-workbench.windows > .part.titlebar > .window-title, @@ -82,16 +82,15 @@ .monaco-workbench > .part.titlebar > .window-controls-container > .window-icon { display: inline-block; -webkit-app-region: no-drag; - -webkit-transition: background-color .2s; - transition: background-color .2s; + -webkit-transition: background-color .1s; + transition: background-color .1s; height: 100%; width: 33.34%; - background-size: 20%; + background-size: 21.74%; background-position: center center; background-repeat: no-repeat; } - .monaco-workbench > .part.titlebar > .window-controls-container > .window-icon svg { shape-rendering: crispEdges; text-align: center; diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 923c1c25051..d7112d84a45 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -31,7 +31,7 @@ import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; import URI from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { addDisposableListener, EventType, EventHelper, Dimension, addClass, removeClass } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom'; import { IPartService } from 'vs/workbench/services/part/common/partService'; export class TitlebarPart extends Part implements ITitleService { @@ -47,6 +47,7 @@ export class TitlebarPart extends Part implements ITitleService { private titleContainer: Builder; private title: Builder; private windowControls: Builder; + private maxRestoreControl: Builder; private appIcon: Builder; private pendingTitle: string; @@ -258,9 +259,6 @@ export class TitlebarPart extends Part implements ITitleService { this.windowService.closeWindow().then(null, errors.onUnexpectedError); }); - - // Resizer - $(this.titleContainer).div({ class: 'resizer' }); } // Title @@ -297,7 +295,7 @@ export class TitlebarPart extends Part implements ITitleService { }); // Restore - $(this.windowControls).div({ class: 'window-icon window-max-restore' }).on(EventType.CLICK, () => { + this.maxRestoreControl = $(this.windowControls).div({ class: 'window-icon window-max-restore' }).on(EventType.CLICK, () => { this.windowService.isMaximized().then((maximized) => { if (maximized) { return this.windowService.unmaximizeWindow(); @@ -315,6 +313,9 @@ export class TitlebarPart extends Part implements ITitleService { const isMaximized = this.windowService.getConfiguration().maximized ? true : false; this.onDidChangeMaximized(isMaximized); this.windowService.onDidChangeMaximize(this.onDidChangeMaximized, this); + + // Resizer + $(this.titleContainer).div({ class: 'resizer' }); } // Since the title area is used to drag the window, we do not want to steal focus from the @@ -332,17 +333,16 @@ export class TitlebarPart extends Part implements ITitleService { } private onDidChangeMaximized(maximized: boolean) { - const element = $(this.titleContainer).getHTMLElement().querySelector('.window-max-restore') as HTMLElement; - if (!element) { + if (!this.maxRestoreControl) { return; } if (maximized) { - removeClass(element, 'window-maximize'); - addClass(element, 'window-unmaximize'); + this.maxRestoreControl.removeClass('window-maximize'); + this.maxRestoreControl.addClass('window-unmaximize'); } else { - removeClass(element, 'window-unmaximize'); - addClass(element, 'window-maximize'); + this.maxRestoreControl.removeClass('window-unmaximize'); + this.maxRestoreControl.addClass('window-maximize'); } } diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index cbde117567a..54306015001 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/views'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, dispose, empty as EmptyDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions'; @@ -109,7 +109,7 @@ export class CustomTreeViewPanel extends ViewletPanel { class TitleMenus implements IDisposable { private disposables: IDisposable[] = []; - private titleDisposable: IDisposable = EmptyDisposable; + private titleDisposable: IDisposable = Disposable.None; private titleActions: IAction[] = []; private titleSecondaryActions: IAction[] = []; @@ -123,7 +123,7 @@ class TitleMenus implements IDisposable { ) { if (this.titleDisposable) { this.titleDisposable.dispose(); - this.titleDisposable = EmptyDisposable; + this.titleDisposable = Disposable.None; } const _contextKeyService = this.contextKeyService.createScoped(); diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 13be029935a..8bfdfdae01c 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -207,12 +207,12 @@ export class PanelViewlet extends Viewlet { super(id, partService, telemetryService, themeService); } - async create(parent: HTMLElement): TPromise { - super.create(parent); - - this.panelview = this._register(new PanelView(parent, this.options)); - this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel))); - this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + this.panelview = this._register(new PanelView(parent, this.options)); + this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel))); + this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); + }); } private showContextMenu(event: StandardMouseEvent): void { diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 217543d55d0..612b59f257f 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -421,8 +421,8 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { this.storageService = storageService; this.contextService = contextService; - this._register(this.onDidAdd(() => this.saveVisibilityStates())); - this._register(this.onDidRemove(() => this.saveVisibilityStates())); + this._register(this.onDidAdd(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); + this._register(this.onDidRemove(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); } saveViewsStates(): void { @@ -436,9 +436,9 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { this.storageService.store(this.viewletStateStorageId, JSON.stringify(storedViewsStates), this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL); } - private saveVisibilityStates(): void { - const storedViewsVisibilityStates: { id: string, isHidden: boolean }[] = []; - for (const viewDescriptor of this.viewDescriptors) { + private saveVisibilityStates(viewDescriptors: IViewDescriptor[]): void { + const storedViewsVisibilityStates = PersistentContributableViewsModel.loadViewsVisibilityState(this.hiddenViewsStorageId, this.storageService, this.contextService); + for (const viewDescriptor of viewDescriptors) { if (viewDescriptor.canToggleVisibility) { const viewState = this.viewStates.get(viewDescriptor.id); storedViewsVisibilityStates.push({ id: viewDescriptor.id, isHidden: viewState ? !viewState.visible : void 0 }); @@ -450,8 +450,7 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { private static loadViewsStates(viewletStateStorageId: string, hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): Map { const viewStates = new Map(); const storedViewsStates = JSON.parse(storageService.get(viewletStateStorageId, contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL, '{}')); - const storedVisibilityStates = >JSON.parse(storageService.get(hiddenViewsStorageId, StorageScope.GLOBAL, '[]')); - const viewsVisibilityStates = <{ id: string, isHidden: boolean }[]>storedVisibilityStates.map(c => typeof c === 'string' /* migration */ ? { id: c, isHidden: true } : c); + const viewsVisibilityStates = PersistentContributableViewsModel.loadViewsVisibilityState(hiddenViewsStorageId, storageService, contextService); for (const { id, isHidden } of viewsVisibilityStates) { const viewState = storedViewsStates[id]; if (viewState) { @@ -469,6 +468,11 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { return viewStates; } + private static loadViewsVisibilityState(hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): { id: string, isHidden: boolean }[] { + const storedVisibilityStates = >JSON.parse(storageService.get(hiddenViewsStorageId, StorageScope.GLOBAL, '[]')); + return <{ id: string, isHidden: boolean }[]>storedVisibilityStates.map(c => typeof c === 'string' /* migration */ ? { id: c, isHidden: true } : c); + } + dispose(): void { this.saveViewsStates(); super.dispose(); diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index e930b4be1fb..183b36edfb6 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -144,31 +144,31 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); - - this._register(this.onDidSashChange(() => this.saveViewSizes())); - this.viewsModel.onDidAdd(added => this.onDidAddViews(added)); - this.viewsModel.onDidRemove(removed => this.onDidRemoveViews(removed)); - const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => { - const size = this.viewsModel.getSize(viewDescriptor.id); - const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id); - return ({ viewDescriptor, index, size, collapsed }); - }); - if (addedViews.length) { - this.onDidAddViews(addedViews); - } - - // Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609 - this.extensionService.whenInstalledExtensionsRegistered().then(() => { - this.areExtensionsReady = true; - if (this.panels.length) { - this.updateTitleArea(); - this.updateViewHeaders(); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + this._register(this.onDidSashChange(() => this.saveViewSizes())); + this.viewsModel.onDidAdd(added => this.onDidAddViews(added)); + this.viewsModel.onDidRemove(removed => this.onDidRemoveViews(removed)); + const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => { + const size = this.viewsModel.getSize(viewDescriptor.id); + const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id); + return ({ viewDescriptor, index, size, collapsed }); + }); + if (addedViews.length) { + this.onDidAddViews(addedViews); } - }); - this.focus(); + // Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609 + this.extensionService.whenInstalledExtensionsRegistered().then(() => { + this.areExtensionsReady = true; + if (this.panels.length) { + this.updateTitleArea(); + this.updateViewHeaders(); + } + }); + + this.focus(); + }); } getContextMenuActions(): IAction[] { @@ -334,8 +334,16 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView }); } - private toggleViewVisibility(id: string): void { - this.viewsModel.setVisible(id, !this.viewsModel.isVisible(id)); + private toggleViewVisibility(viewId: string): void { + const visible = !this.viewsModel.isVisible(viewId); + /* __GDPR__ + "views.toggleVisibility" : { + "viewId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "visible": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('views.toggledVisibility', { viewId, visible }); + this.viewsModel.setVisible(viewId, visible); } private saveViewSizes(): void { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 2a9380282e9..e49e2d26fc5 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -395,11 +395,9 @@ export abstract class EditorInput extends Disposable implements IEditorInput { /** * Returns a type of EditorModel that represents the resolved input. Subclasses should - * override to provide a meaningful model. The optional second argument allows to specify - * if the EditorModel should be refreshed before returning it. Depending on the implementation - * this could mean to refresh the editor model contents with the version from disk. + * override to provide a meaningful model. */ - abstract resolve(refresh?: boolean): TPromise; + abstract resolve(): TPromise; /** * An editor that is dirty will be asked to be saved once it closes. @@ -582,7 +580,7 @@ export class SideBySideEditorInput extends EditorInput { this._register(this.master.onDidChangeLabel(() => this._onDidChangeLabel.fire())); } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } @@ -677,7 +675,7 @@ export class EditorOptions implements IEditorOptions { const options = new EditorOptions(); options.preserveFocus = settings.preserveFocus; - options.forceOpen = settings.forceOpen; + options.forceReload = settings.forceReload; options.revealIfVisible = settings.revealIfVisible; options.revealIfOpened = settings.revealIfOpened; options.pinned = settings.pinned; @@ -694,11 +692,11 @@ export class EditorOptions implements IEditorOptions { preserveFocus: boolean; /** - * Tells the editor to replace the editor input in the editor even if it is identical to the one - * already showing. By default, the editor will not replace the input if it is identical to the + * Tells the editor to reload the editor input in the editor even if it is identical to the one + * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ - forceOpen: boolean; + forceReload: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. @@ -763,8 +761,8 @@ export class TextEditorOptions extends EditorOptions { textEditorOptions.editorViewState = options.viewState as IEditorViewState; } - if (options.forceOpen) { - textEditorOptions.forceOpen = true; + if (options.forceReload) { + textEditorOptions.forceReload = true; } if (options.revealIfVisible) { diff --git a/src/vs/workbench/common/editor/dataUriEditorInput.ts b/src/vs/workbench/common/editor/dataUriEditorInput.ts index cc8bb4cb10c..07f9455bd3f 100644 --- a/src/vs/workbench/common/editor/dataUriEditorInput.ts +++ b/src/vs/workbench/common/editor/dataUriEditorInput.ts @@ -65,7 +65,7 @@ export class DataUriEditorInput extends EditorInput { return this.description; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel); } diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 7625ffdf06f..36f7632e566 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -36,23 +36,13 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.master; } - resolve(refresh?: boolean): TPromise { - let modelPromise: TPromise; - - // Use Cached Model - if (this.cachedModel && !refresh) { - modelPromise = TPromise.as(this.cachedModel); - } + resolve(): TPromise { // Create Model - we never reuse our cached model if refresh is true because we cannot // decide for the inputs within if the cached model can be reused or not. There may be // inputs that need to be loaded again and thus we always recreate the model and dispose // the previous one - if any. - else { - modelPromise = this.createModel(refresh); - } - - return modelPromise.then((resolvedModel: DiffEditorModel) => { + return this.createModel().then(resolvedModel => { if (this.cachedModel) { this.cachedModel.dispose(); } @@ -71,9 +61,9 @@ export class DiffEditorInput extends SideBySideEditorInput { // Join resolve call over two inputs and build diff editor model return TPromise.join([ - this.originalInput.resolve(refresh), - this.modifiedInput.resolve(refresh) - ]).then((models) => { + this.originalInput.resolve(), + this.modifiedInput.resolve() + ]).then(models => { const originalEditorModel = models[0]; const modifiedEditorModel = models[1]; diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 44a3789a400..572510986ac 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -82,7 +82,7 @@ export class ResourceEditorInput extends EditorInput { return descriptor; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { if (!this.modelReference) { this.modelReference = this.textModelResolverService.createModelReference(this.resource); } diff --git a/src/vs/workbench/electron-browser/bootstrap/index.js b/src/vs/workbench/electron-browser/bootstrap/index.js index 75f6dc4d7f6..692122631e7 100644 --- a/src/vs/workbench/electron-browser/bootstrap/index.js +++ b/src/vs/workbench/electron-browser/bootstrap/index.js @@ -81,6 +81,8 @@ function readFile(file) { }); } +const writeFile = (file, content) => new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); + function registerListeners(enableDeveloperTools) { // Devtools & reload support @@ -180,8 +182,15 @@ function main() { let json = JSON.parse(content); bundles[bundle] = json; cb(undefined, json); - }) - .catch(cb); + }).catch((error) => { + try { + if (nlsConfig._corruptedFile) { + writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + } + } finally { + cb(error, undefined); + } + }); }; } diff --git a/src/vs/workbench/electron-browser/media/shell.css b/src/vs/workbench/electron-browser/media/shell.css index 3b5709bfcac..64869f0de97 100644 --- a/src/vs/workbench/electron-browser/media/shell.css +++ b/src/vs/workbench/electron-browser/media/shell.css @@ -69,11 +69,16 @@ padding: .5em 0; } -.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label, +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), .monaco-shell .monaco-menu .monaco-action-bar.vertical .keybinding { padding: 0.5em 2em; } +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label.separator { + padding: 0.2em 0 0 0; + margin-bottom: 0.2em; +} + .monaco-shell .monaco-menu .monaco-action-bar.vertical .submenu-indicator { padding: 0.5em 1em; } diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 830a3843c3e..1096104def6 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -455,6 +455,15 @@ export class WorkbenchShell extends Disposable { open(): void { + // Listen on unhandled rejection events + window.addEventListener('unhandledrejection', (event) => { + // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + errors.onUnexpectedError((event).reason); + + // Prevent the printing of this event to the console + event.preventDefault(); + }); + // Listen on unexpected errors errors.setUnexpectedErrorHandler((error: any) => { this.onUnexpectedError(error); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index ccde056f231..a87a8f73f31 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -8,14 +8,13 @@ import * as nls from 'vs/nls'; import URI from 'vs/base/common/uri'; import * as errors from 'vs/base/common/errors'; -import * as types from 'vs/base/common/types'; import { TPromise } from 'vs/base/common/winjs.base'; import * as arrays from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import * as DOM from 'vs/base/browser/dom'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, Action } from 'vs/base/common/actions'; -import { AutoSaveConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -38,7 +37,6 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { fillInActionBarActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { IIntegrityService } from 'vs/platform/integrity/common/integrity'; @@ -60,8 +58,6 @@ const TextInputActions: IAction[] = [ export class ElectronWindow extends Themable { - private static readonly AUTO_SAVE_SETTING = 'files.autoSave'; - private touchBarMenu: IMenu; private touchBarUpdater: RunOnceScheduler; private touchBarDisposables: IDisposable[]; @@ -181,11 +177,6 @@ export class ElectronWindow extends Themable { this.notificationService.info(message); }); - // Support toggling auto save - ipc.on('vscode.toggleAutoSave', () => { - this.toggleAutoSave(); - }); - // Fullscreen Events ipc.on('vscode:enterFullScreen', () => { this.lifecycleService.when(LifecyclePhase.Running).then(() => { @@ -507,23 +498,6 @@ export class ElectronWindow extends Themable { }); } - private toggleAutoSave(): void { - const setting = this.configurationService.inspect(ElectronWindow.AUTO_SAVE_SETTING); - let userAutoSaveConfig = setting.user; - if (types.isUndefinedOrNull(userAutoSaveConfig)) { - userAutoSaveConfig = setting.default; // use default if setting not defined - } - - let newAutoSaveValue: string; - if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) { - newAutoSaveValue = AutoSaveConfiguration.OFF; - } else { - newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY; - } - - this.configurationService.updateValue(ElectronWindow.AUTO_SAVE_SETTING, newAutoSaveValue, ConfigurationTarget.USER); - } - dispose(): void { this.touchBarDisposables = dispose(this.touchBarDisposables); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 6c3e8ab23bd..01b3628a51a 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -707,7 +707,12 @@ export class Workbench extends Disposable implements IPartService { const panelRegistry = Registry.as(PanelExtensions.Panels); const panelId = this.storageService.get(PanelPart.activePanelSettingsKey, StorageScope.WORKSPACE, panelRegistry.getDefaultPanelId()); if (!this.panelHidden && !!panelId) { - restorePromises.push(this.panelPart.openPanel(panelId, false)); + const isPanelToRestoreEnabled = !!this.panelPart.getPanels().filter(p => p.id === panelId).length; + if (isPanelToRestoreEnabled) { + restorePromises.push(this.panelPart.openPanel(panelId, false)); + } else { + restorePromises.push(this.panelPart.openPanel(panelRegistry.getDefaultPanelId(), false)); + } } // Restore Zen Mode if active @@ -1205,7 +1210,7 @@ export class Workbench extends Disposable implements IPartService { getTitleBarOffset(): number { let offset = 0; if (this.isVisible(Parts.TITLEBAR_PART)) { - offset = 22 / browser.getZoomFactor(); // adjust the position based on title bar size and zoom factor + offset = this.workbenchLayout.partLayoutInfo.titlebar.height; } return offset; diff --git a/src/vs/workbench/node/extensionHostMain.ts b/src/vs/workbench/node/extensionHostMain.ts index 4aa8811e6f3..6bf7a628602 100644 --- a/src/vs/workbench/node/extensionHostMain.ts +++ b/src/vs/workbench/node/extensionHostMain.ts @@ -234,7 +234,7 @@ export class ExtensionHostMain { return TPromise.join([fileNamePromise, globPatternPromise]).then(() => { }); } - private async activateIfFileName(extensionId: string, fileName: string): TPromise { + private async activateIfFileName(extensionId: string, fileName: string): Promise { // find exact path for (const { uri } of this._workspace.folders) { @@ -250,7 +250,7 @@ export class ExtensionHostMain { return undefined; } - private async activateIfGlobPatterns(extensionId: string, globPatterns: string[]): TPromise { + private async activateIfGlobPatterns(extensionId: string, globPatterns: string[]): Promise { this._extHostLogService.trace(`extensionHostMain#activateIfGlobPatterns: fileSearch, extension: ${extensionId}, entryPoint: workspaceContains`); if (globPatterns.length === 0) { diff --git a/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts b/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts index 5e754fb953d..d4526043633 100644 --- a/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts +++ b/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts @@ -29,12 +29,13 @@ let _source: string = null; function getSource(): string { if (!_source) { const root = URI.parse(require.toUrl('')).fsPath; - _source = path.resolve(root, '..', 'bin', 'code'); + _source = path.resolve(root, '..', 'resources', 'darwin', 'bin', 'code.sh'); } return _source; } function isAvailable(): TPromise { + console.log(getSource()); return pfs.exists(getSource()); } @@ -70,20 +71,16 @@ class InstallAction extends Action { if (!isAvailable || isInstalled) { return TPromise.as(null); } else { - const createSymlink = () => { - return pfs.unlink(this.target) - .then(null, ignore('ENOENT')) - .then(() => pfs.symlink(getSource(), this.target)); - }; + return pfs.unlink(this.target) + .then(null, ignore('ENOENT')) + .then(() => pfs.symlink(getSource(), this.target)) + .then(null, err => { + if (err.code === 'EACCES' || err.code === 'ENOENT') { + return this.createBinFolderAndSymlinkAsAdmin(); + } - return createSymlink().then(null, err => { - if (err.code === 'EACCES' || err.code === 'ENOENT') { - return this.createBinFolder() - .then(() => createSymlink()); - } - - return TPromise.wrapError(err); - }); + return TPromise.wrapError(err); + }); } }) .then(() => { @@ -101,14 +98,14 @@ class InstallAction extends Action { .then(null, ignore('ENOENT', false)); } - private createBinFolder(): TPromise { + private createBinFolderAndSymlinkAsAdmin(): TPromise { return new TPromise((c, e) => { const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; this.dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(choice => { switch (choice) { case 0 /* OK */: - const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && chown \\" & (do shell script (\\"whoami\\")) & \\" /usr/local/bin\\" with administrator privileges"'; + const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + this.target + '\'\\" with administrator privileges"'; nfcall(cp.exec, command, {}) .then(null, _ => TPromise.wrapError(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) @@ -132,7 +129,8 @@ class UninstallAction extends Action { id: string, label: string, @INotificationService private notificationService: INotificationService, - @ILogService private logService: ILogService + @ILogService private logService: ILogService, + @IDialogService private dialogService: IDialogService ) { super(id, label); } @@ -149,12 +147,42 @@ class UninstallAction extends Action { return undefined; } - return pfs.unlink(this.target) - .then(null, ignore('ENOENT')) - .then(() => { - this.logService.trace('cli#uninstall', this.target); - this.notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", product.applicationName)); - }); + const uninstall = () => { + return pfs.unlink(this.target) + .then(null, ignore('ENOENT')); + }; + + return uninstall().then(null, err => { + if (err.code === 'EACCES') { + return this.deleteSymlinkAsAdmin(); + } + + return TPromise.wrapError(err); + }).then(() => { + this.logService.trace('cli#uninstall', this.target); + this.notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", product.applicationName)); + }); + }); + } + + private deleteSymlinkAsAdmin(): TPromise { + return new TPromise((c, e) => { + const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; + + this.dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }).then(choice => { + switch (choice) { + case 0 /* OK */: + const command = 'osascript -e "do shell script \\"rm \'' + this.target + '\'\\" with administrator privileges"'; + + nfcall(cp.exec, command, {}) + .then(null, _ => TPromise.wrapError(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", this.target)))) + .done(c, e); + break; + case 1 /* Cancel */: + e(new Error(nls.localize('aborted', "Aborted"))); + break; + } + }); }); } } diff --git a/src/vs/workbench/parts/comments/electron-browser/commentService.ts b/src/vs/workbench/parts/comments/electron-browser/commentService.ts index 80aa7b9359e..ff0fe6655d1 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentService.ts @@ -115,7 +115,7 @@ export class CommentService extends Disposable implements ICommentService { return null; } - async getComments(resource: URI): TPromise { + getComments(resource: URI): TPromise { const result = []; for (const handle of keys(this._commentProviders)) { const provider = this._commentProviders.get(handle); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index 7ff7d99f135..1ee06237e2e 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -64,7 +64,6 @@ export class CommentNode { this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); this._domNode.setAttribute('role', 'treeitem'); - this._domNode.title = `${comment.userName}, ${comment.body.value}`; this._clearTimeout = null; } @@ -109,7 +108,6 @@ export class ReviewZoneWidget extends ZoneWidget { private _commentThread: modes.CommentThread; private _commentGlyph: CommentGlyphWidget; private _owner: number; - private _decorationIDs: string[]; private _localToDispose: IDisposable[]; public get owner(): number { @@ -135,7 +133,6 @@ export class ReviewZoneWidget extends ZoneWidget { this._owner = owner; this._commentThread = commentThread; this._isCollapsed = commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded; - this._decorationIDs = []; this._localToDispose = []; this.create(); this.themeService.onThemeChange(this._applyTheme, this); @@ -151,12 +148,7 @@ export class ReviewZoneWidget extends ZoneWidget { public reveal(commentId?: string) { if (this._isCollapsed) { - if (this._decorationIDs && this._decorationIDs.length) { - let range = this.editor.getModel().getDecorationRange(this._decorationIDs[0]); - this.show(range, 2); - } else { - this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); - } + this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); } this._bodyElement.focus(); @@ -196,13 +188,9 @@ export class ReviewZoneWidget extends ZoneWidget { this._secondaryHeading = $('span.dirname').appendTo(titleElement).getHTMLElement(); this._metaHeading = $('span.meta').appendTo(titleElement).getHTMLElement(); - let primaryHeading = 'Participants:'; - $(this._primaryHeading).safeInnerHtml(primaryHeading); - this._primaryHeading.setAttribute('aria-label', primaryHeading); - - let secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); - $(this._secondaryHeading).safeInnerHtml(secondaryHeading); - this._secondaryHeading.setAttribute('aria-label', secondaryHeading); + if (this._commentThread.comments.length) { + this.createParticipantsLabel(); + } const actionsContainer = $('.review-actions').appendTo(this._headElement); this._actionbarWidget = new ActionBar(actionsContainer.getHTMLElement(), {}); @@ -254,16 +242,6 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentsElement.removeChild(commentElementsToDel[i].domNode); } - if (this._commentElements.length === 0) { - this._commentThread = commentThread; - commentThread.comments.forEach(comment => { - let newElement = new CommentNode(comment); - this._commentElements.push(newElement); - this._commentsElement.appendChild(newElement.domNode); - }); - return; - } - let lastCommentElement: HTMLElement = null; let newCommentNodeList: CommentNode[] = []; for (let i = newCommentsLen - 1; i >= 0; i--) { @@ -350,7 +328,15 @@ export class ReviewZoneWidget extends ZoneWidget { this.setCommentEditorDecorations(); // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments - this.createCommentButton(); + if (hasExistingComments) { + this.createReplyButton(); + } else { + if (!dom.hasClass(this._commentForm, 'expand')) { + dom.addClass(this._commentForm, 'expand'); + this._commentEditor.focus(); + } + } + this._localToDispose.push(this._commentEditor.onKeyDown((ev: IKeyboardEvent) => { const hasExistingComments = this._commentThread.comments.length > 0; @@ -384,6 +370,9 @@ export class ReviewZoneWidget extends ZoneWidget { new Range(lineNumber, 1, lineNumber, 1), this._commentEditor.getValue() ); + + this.createReplyButton(); + this.createParticipantsLabel(); } this._commentEditor.setValue(''); @@ -415,28 +404,33 @@ export class ReviewZoneWidget extends ZoneWidget { } } - createCommentButton() { - const hasExistingComments = this._commentThread.comments.length > 0; - if (hasExistingComments) { - this._reviewThreadReplyButton = $('button.review-thread-reply-button').appendTo(this._commentForm).getHTMLElement(); - this._reviewThreadReplyButton.title = 'Reply...'; - this._reviewThreadReplyButton.textContent = 'Reply...'; - // bind click/escape actions for reviewThreadReplyButton and textArea - this._reviewThreadReplyButton.onclick = () => { - if (!dom.hasClass(this._commentForm, 'expand')) { - dom.addClass(this._commentForm, 'expand'); - this._commentEditor.focus(); - } - }; + createParticipantsLabel() { + const primaryHeading = 'Participants:'; + $(this._primaryHeading).safeInnerHtml(primaryHeading); + this._primaryHeading.setAttribute('aria-label', primaryHeading); - this._commentEditor.onDidBlurEditorWidget(() => { - if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(this._commentForm, 'expand')) { - dom.removeClass(this._commentForm, 'expand'); - } - }); - } else { - dom.addClass(this._commentForm, 'expand'); - } + const secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); + $(this._secondaryHeading).safeInnerHtml(secondaryHeading); + this._secondaryHeading.setAttribute('aria-label', secondaryHeading); + } + + createReplyButton() { + this._reviewThreadReplyButton = $('button.review-thread-reply-button').appendTo(this._commentForm).getHTMLElement(); + this._reviewThreadReplyButton.title = 'Reply...'; + this._reviewThreadReplyButton.textContent = 'Reply...'; + // bind click/escape actions for reviewThreadReplyButton and textArea + this._reviewThreadReplyButton.onclick = () => { + if (!dom.hasClass(this._commentForm, 'expand')) { + dom.addClass(this._commentForm, 'expand'); + this._commentEditor.focus(); + } + }; + + this._commentEditor.onDidBlurEditorWidget(() => { + if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(this._commentForm, 'expand')) { + dom.removeClass(this._commentForm, 'expand'); + } + }); } _refresh() { @@ -557,13 +551,12 @@ export class ReviewZoneWidget extends ZoneWidget { this._resizeObserver.disconnect(); this._resizeObserver = null; } - this.editor.changeDecorations(accessor => { - accessor.deltaDecorations(this._decorationIDs, []); - }); + if (this._commentGlyph) { this.editor.removeContentWidget(this._commentGlyph); this._commentGlyph = null; } + this._localToDispose.forEach(local => local.dispose()); this._onDidClose.fire(); } diff --git a/src/vs/workbench/parts/debug/browser/breakpointsView.ts b/src/vs/workbench/parts/debug/browser/breakpointsView.ts index ba6e7cb7b92..77f2cd56085 100644 --- a/src/vs/workbench/parts/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/browser/breakpointsView.ts @@ -35,7 +35,7 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; const $ = dom.$; @@ -58,7 +58,7 @@ export class BreakpointsView extends ViewletPanel { @IContextViewService private contextViewService: IContextViewService, @IConfigurationService configurationService: IConfigurationService ) { - super(options, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService); this.minimumBodySize = this.maximumBodySize = this.getExpandedBodySize(); this.settings = options.viewletSettings; diff --git a/src/vs/workbench/parts/debug/browser/debugStatus.ts b/src/vs/workbench/parts/debug/browser/debugStatus.ts index 970cce4f2cd..149c9bd8eb8 100644 --- a/src/vs/workbench/parts/debug/browser/debugStatus.ts +++ b/src/vs/workbench/parts/debug/browser/debugStatus.ts @@ -76,7 +76,7 @@ export class DebugStatus extends Themable implements IStatusbarItem { private doRender(): void { if (!this.statusBarItem && this.container) { this.statusBarItem = dom.append(this.container, $('.debug-statusbar-item')); - this.toDispose.push(dom.addDisposableListener(this.statusBarItem, 'click', () => { + this._register(dom.addDisposableListener(this.statusBarItem, 'click', () => { this.quickOpenService.show('debug ').done(undefined, errors.onUnexpectedError); })); this.statusBarItem.title = nls.localize('selectAndStartDebug', "Select and start debug configuration"); diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index 2cc3d77ddf9..30ffc30d939 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -66,10 +66,10 @@ export class DebugViewlet extends ViewContainerViewlet { })); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); - - DOM.addClass(parent, 'debug-viewlet'); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + DOM.addClass(parent, 'debug-viewlet'); + }); } public focus(): void { diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts index bbf587bfb00..69c19b227fc 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts @@ -27,9 +27,7 @@ import uri from 'vs/base/common/uri'; import { SuggestRegistry, ISuggestResult, SuggestContext } from 'vs/editor/common/modes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; -import { wireCancellationToken } from 'vs/base/common/async'; import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -218,9 +216,9 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.toDispose.push(SuggestRegistry.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, { provideCompletionItems: (model: ITextModel, position: Position, _context: SuggestContext, token: CancellationToken): Thenable => { - let suggestionsPromise: TPromise; + let suggestionsPromise: Promise; if (this.context === Context.CONDITION || this.context === Context.LOG_MESSAGE && this.isCurlyBracketOpen()) { - suggestionsPromise = provideSuggestionItems(this.editor.getModel(), new Position(this.lineNumber, 1), 'none', undefined, _context).then(suggestions => { + suggestionsPromise = provideSuggestionItems(this.editor.getModel(), new Position(this.lineNumber, 1), 'none', undefined, _context, token).then(suggestions => { let overwriteBefore = 0; if (this.context === Context.CONDITION) { @@ -242,10 +240,10 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi }; }); } else { - suggestionsPromise = TPromise.as({ suggestions: [] }); + suggestionsPromise = Promise.resolve({ suggestions: [] }); } - return wireCancellationToken(token, suggestionsPromise); + return suggestionsPromise; } })); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 40826f4af81..3daf1bd37ca 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -486,7 +486,6 @@ class Launch implements ILaunch { return this.editorService.openEditor({ resource: resource, options: { - forceOpen: true, selection, pinned: created, revealIfVisible: true diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 60d0dda6202..992050fd338 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -46,6 +46,7 @@ import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { memoize } from 'vs/base/common/decorators'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { getHover } from 'vs/editor/contrib/hover/getHover'; +import { IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; const HOVER_DELAY = 300; const LAUNCH_JSON_REGEX = /launch\.json$/; @@ -281,7 +282,9 @@ export class DebugEditorContribution implements IDebugEditorContribution { private _applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame): void { if (stackFrame && model && model.uri.toString() === stackFrame.source.uri.toString()) { this.editor.updateOptions({ - hover: !stackFrame || !model || model.uri.toString() !== stackFrame.source.uri.toString() + hover: { + enabled: !stackFrame || !model || model.uri.toString() !== stackFrame.source.uri.toString() + } }); } else { let overrides: IConfigurationOverrides; @@ -291,9 +294,13 @@ export class DebugEditorContribution implements IDebugEditorContribution { overrideIdentifier: model.getLanguageIdentifier().language }; } - const defaultConfiguration = this.configurationService.getValue('editor.hover', overrides); + const defaultConfiguration = this.configurationService.getValue('editor.hover', overrides); this.editor.updateOptions({ - hover: defaultConfiguration + hover: { + enabled: defaultConfiguration.enabled, + delay: defaultConfiguration.delay, + sticky: defaultConfiguration.sticky + } }); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 645efdd8418..73740d649f3 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -428,43 +428,46 @@ export class RawDebugSession implements debug.IRawSession { private dispatchRequest(request: DebugProtocol.Request): void { - const response: DebugProtocol.Response = { - type: 'response', - seq: 0, - command: request.command, - request_seq: request.seq, - success: true - }; + if (this.debugAdapter) { - if (request.command === 'runInTerminal') { + const response: DebugProtocol.Response = { + type: 'response', + seq: 0, + command: request.command, + request_seq: request.seq, + success: true + }; - this._debugger.runInTerminal(request.arguments).then(_ => { - response.body = {}; - this.debugAdapter.sendResponse(response); - }, err => { + if (request.command === 'runInTerminal') { + + this._debugger.runInTerminal(request.arguments).then(_ => { + response.body = {}; + this.debugAdapter.sendResponse(response); + }, err => { + response.success = false; + response.message = err.message; + this.debugAdapter.sendResponse(response); + }); + + } else if (request.command === 'handshake') { + try { + const vsda = require.__$__nodeRequire('vsda'); + const obj = new vsda.signer(); + const sig = obj.sign(request.arguments.value); + response.body = { + signature: sig + }; + this.debugAdapter.sendResponse(response); + } catch (e) { + response.success = false; + response.message = e.message; + this.debugAdapter.sendResponse(response); + } + } else { response.success = false; - response.message = err.message; - this.debugAdapter.sendResponse(response); - }); - - } else if (request.command === 'handshake') { - try { - const vsda = require.__$__nodeRequire('vsda'); - const obj = new vsda.signer(); - const sig = obj.sign(request.arguments.value); - response.body = { - signature: sig - }; - this.debugAdapter.sendResponse(response); - } catch (e) { - response.success = false; - response.message = e.message; + response.message = `unknown request '${request.command}'`; this.debugAdapter.sendResponse(response); } - } else { - response.success = false; - response.message = `unknown request '${request.command}'`; - this.debugAdapter.sendResponse(response); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts index f05ab8af4a4..91fc2c53456 100644 --- a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts +++ b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts @@ -22,7 +22,7 @@ export class TerminalLauncher implements ITerminalLauncher { ) { } - async runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { if (args.kind === 'external') { return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); diff --git a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts index c1b3079a4c2..5a517047652 100644 --- a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts @@ -50,7 +50,7 @@ export class WatchExpressionsView extends TreeViewsViewletPanel { @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('expressionsSection', "Expressions Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService); this.settings = options.viewletSettings; this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { diff --git a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts index 3bd97be51bb..00f631a566e 100644 --- a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts +++ b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts @@ -129,15 +129,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: OPEN_NATIVE_CONSOLE_COMMAND_ID, - title: env.isWindows ? nls.localize('globalConsoleActionWin', "Open New Command Prompt") : - nls.localize('globalConsoleActionMacLinux', "Open New Terminal") + title: nls.localize('globalConsoleAction', "Open New Terminal") } }); const openConsoleCommand = { id: OPEN_IN_TERMINAL_COMMAND_ID, - title: env.isWindows ? nls.localize('scopedConsoleActionWin', "Open in Command Prompt") : - nls.localize('scopedConsoleActionMacLinux', "Open in Terminal") + title: nls.localize('scopedConsoleAction', "Open in Terminal") }; MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', diff --git a/src/vs/workbench/parts/extensions/common/extensionsInput.ts b/src/vs/workbench/parts/extensions/common/extensionsInput.ts index 23d3e199d16..3a7642cd39a 100644 --- a/src/vs/workbench/parts/extensions/common/extensionsInput.ts +++ b/src/vs/workbench/parts/extensions/common/extensionsInput.ts @@ -44,7 +44,7 @@ export class ExtensionsInput extends EditorInput { return this.extension === otherExtensionInput.extension; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index 087b2553530..88964425ea0 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -1290,6 +1290,7 @@ export class ReloadAction extends Action { @IWindowService private windowService: IWindowService, @IExtensionService private extensionService: IExtensionService, @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); this.throttler = new Throttler(); @@ -1325,7 +1326,9 @@ export class ReloadAction extends Action { if (installed && installed.local) { if (runningExtension) { - const isSameLocation = runningExtension.extensionLocation.toString() === installed.local.location.toString(); + const runningExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation); + const installedExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(installed.local.location); + const isSameLocation = runningExtensionServer.location.toString() === installedExtensionServer.location.toString(); if (isSameLocation) { const isDifferentVersionRunning = this.extension.version !== runningExtension.version; if (isDifferentVersionRunning && !isDisabled) { @@ -2041,7 +2044,6 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio .then(selection => this.editorService.openEditor({ resource: extensionsFileResource, options: { - forceOpen: true, pinned: created, selection } @@ -2055,8 +2057,8 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio .then(selection => this.editorService.openEditor({ resource: workspaceConfigurationFile, options: { - forceOpen: true, - selection + selection, + forceReload: true // because content has changed } })); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts index f90c3f99801..1f032611899 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -154,7 +154,13 @@ export class Renderer implements IPagedRenderer { const installed = this.extensionsWorkbenchService.local.filter(e => e.id === extension.id)[0]; this.extensionService.getExtensions().then(runningExtensions => { - toggleClass(data.root, 'disabled', installed && installed.local ? runningExtensions.every(e => !(installed.local.location.toString() === e.extensionLocation.toString() && areSameExtensions(e, extension))) : false); + if (installed && installed.local) { + const installedExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(installed.local.location); + const isSameExtensionRunning = runningExtensions.some(e => areSameExtensions(e, extension) && installedExtensionServer.location.toString() === this.extensionManagementServerService.getExtensionManagementServer(e.extensionLocation).location.toString()); + toggleClass(data.root, 'disabled', !isSameExtensionRunning); + } else { + removeClass(data.root, 'disabled'); + } }); const onError = once(domEvent(data.icon, 'error')); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 40be6b379cd..507bf50decd 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -301,7 +301,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio }, this, this.disposables); } - async create(parent: HTMLElement): TPromise { + create(parent: HTMLElement): TPromise { addClass(parent, 'extensions-viewlet'); this.root = parent; @@ -326,14 +326,14 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.onSearchChange = mapEvent(onSearchInput, e => e.target.value); - await super.create(this.extensionsBox); - - const installed = await this.extensionManagementService.getInstalled(LocalExtensionType.User); - - if (installed.length === 0) { - this.searchBox.value = '@sort:installs'; - this.searchExtensionsContextKey.set(true); - } + return super.create(this.extensionsBox) + .then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User)) + .then(installed => { + if (installed.length === 0) { + this.searchBox.value = '@sort:installs'; + this.searchExtensionsContextKey.set(true); + } + }); } public updateStyles(): void { @@ -429,7 +429,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio .done(null, err => this.onError(err)); } - private async doSearch(): TPromise { + private doSearch(): TPromise { const value = this.searchBox.value || ''; this.searchExtensionsContextKey.set(!!value); this.searchInstalledExtensionsContextKey.set(InstalledExtensionsView.isInstalledExtensionsQuery(value)); @@ -439,8 +439,9 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); if (value) { - this.progress(TPromise.join(this.panels.map(view => (view).show(this.searchBox.value)))); + return this.progress(TPromise.join(this.panels.map(view => (view).show(this.searchBox.value)))); } + return TPromise.as(null); } protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index e61c9d9603a..a03d325d251 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -107,7 +107,7 @@ export class ExtensionsListView extends ViewletPanel { this.list.layout(size); } - async show(query: string): TPromise> { + async show(query: string): Promise> { const model = await this.query(query); this.setModel(model); return model; @@ -141,7 +141,7 @@ export class ExtensionsListView extends ViewletPanel { return this.list.length; } - private async query(value: string): TPromise> { + private async query(value: string): Promise> { const query = Query.parse(value); let options: IQueryOptions = { @@ -652,7 +652,7 @@ export class InstalledExtensionsView extends ExtensionsListView { || ExtensionsListView.isEnabledExtensionsQuery(query); } - async show(query: string): TPromise> { + async show(query: string): Promise> { if (InstalledExtensionsView.isInstalledExtensionsQuery(query)) { return super.show(query); } @@ -664,7 +664,7 @@ export class InstalledExtensionsView extends ExtensionsListView { export class GroupByServerExtensionsView extends ExtensionsListView { - async show(query: string): TPromise> { + async show(query: string): Promise> { query = query.replace(/@group:server/g, '').trim(); query = query ? query : '@installed'; if (!InstalledExtensionsView.isInstalledExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { @@ -676,21 +676,21 @@ export class GroupByServerExtensionsView extends ExtensionsListView { export class EnabledExtensionsView extends ExtensionsListView { - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show('@enabled'); } } export class DisabledExtensionsView extends ExtensionsListView { - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show('@disabled'); } } export class BuiltInExtensionsView extends ExtensionsListView { - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show(query.replace('@builtin', '@builtin:features')); } @@ -698,14 +698,14 @@ export class BuiltInExtensionsView extends ExtensionsListView { export class BuiltInThemesExtensionsView extends ExtensionsListView { - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show(query.replace('@builtin', '@builtin:themes')); } } export class BuiltInBasicsExtensionsView extends ExtensionsListView { - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show(query.replace('@builtin', '@builtin:basics')); } } @@ -720,7 +720,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView { })); } - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show('@recommended:all'); } @@ -737,7 +737,7 @@ export class RecommendedExtensionsView extends ExtensionsListView { })); } - async show(query: string): TPromise> { + async show(query: string): Promise> { return super.show('@recommended'); } } @@ -777,7 +777,7 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this.disposables.push(...[this.installAllAction, configureWorkspaceFolderAction, actionbar]); } - async show(): TPromise> { + async show(): Promise> { let model = await super.show('@recommended:workspace'); this.setExpanded(model.length > 0); return model; diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index ae90082e58a..ed9c80d3b1a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -443,7 +443,7 @@ export class RuntimeExtensionsInput extends EditorInput { return true; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } @@ -573,7 +573,11 @@ class SaveExtensionHostProfileAction extends Action { }); } - async run(): TPromise { + run(): TPromise { + return TPromise.wrap(this._asyncRun()); + } + + private async _asyncRun(): Promise { let picked = await this._windowService.showSaveDialog({ title: 'Save Extension Host Profile', buttonLabel: 'Save', diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index b09e7e2b83e..03a9cc7540c 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -753,7 +753,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, for (const extension of extensions) { let dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local, enablementState); if (dependents.length) { - return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, dependents))); + return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, allExtensions, dependents))); } } } @@ -806,7 +806,17 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, }); } - private getDependentsErrorMessage(extension: IExtension, dependents: IExtension[]): string { + private getDependentsErrorMessage(extension: IExtension, allDisabledExtensions: IExtension[], dependents: IExtension[]): string { + for (const e of [extension, ...allDisabledExtensions]) { + let dependentsOfTheExtension = dependents.filter(d => d.dependencies.some(id => areSameExtensions({ id }, e))); + if (dependentsOfTheExtension.length) { + return this.getErrorMessageForDisablingAnExtensionWithDependents(e, dependentsOfTheExtension); + } + } + return ''; + } + + private getErrorMessageForDisablingAnExtensionWithDependents(extension: IExtension, dependents: IExtension[]): string { if (dependents.length === 1) { return nls.localize('singleDependentError', "Cannot disable extension '{0}'. Extension '{1}' depends on this.", extension.displayName, dependents[0].displayName); } @@ -964,13 +974,13 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, this.notificationService.error(err); } - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { if (!/^extension/.test(uri.path)) { - return false; + return TPromise.as(false); } this.onOpenExtensionUrl(uri); - return true; + return TPromise.as(true); } private onOpenExtensionUrl(uri: URI): void { diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts index 8308b8fc640..926f041931e 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts @@ -14,7 +14,7 @@ import * as ExtensionsActions from 'vs/workbench/parts/extensions/electron-brows import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, LocalExtensionType, IGalleryExtension, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService, getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -35,7 +35,8 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { URLService } from 'vs/platform/url/common/urlService'; import URI from 'vs/base/common/uri'; -import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { SingleServerExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { Schemas } from 'vs/base/common/network'; suite('ExtensionsActions Test', () => { @@ -70,7 +71,7 @@ suite('ExtensionsActions Test', () => { instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); - instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(ExtensionManagementServerService, instantiationService.get(IExtensionManagementService))); + instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(SingleServerExtensionManagementServerService, { location: URI.from({ scheme: Schemas.file }), extensionManagementService: instantiationService.get(IExtensionManagementService) })); instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); diff --git a/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts index df5857ced33..8b16509b50c 100644 --- a/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts @@ -6,7 +6,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import * as errors from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; @@ -20,7 +19,6 @@ import { distinct } from 'vs/base/common/arrays'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isLinux } from 'vs/base/common/platform'; -import { ResourceQueue } from 'vs/base/common/async'; import { ResourceMap } from 'vs/base/common/map'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -29,6 +27,8 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ResourceQueue } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class FileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -125,7 +125,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private onFileChanges(e: FileChangesEvent): void { // Handle updates - this.handleUpdates(e); + if (e.gotAdded() || e.gotUpdated()) { + this.handleUpdates(e); + } // Handle deletes if (e.gotDeleted()) { @@ -286,31 +288,11 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private handleUpdates(e: FileChangesEvent): void { - // Handle updates to visible binary editors - this.handleUpdatesToVisibleBinaryEditors(e); - // Handle updates to text models this.handleUpdatesToTextModels(e); - } - private handleUpdatesToVisibleBinaryEditors(e: FileChangesEvent): void { - const editors = this.editorService.visibleControls; - editors.forEach(editor => { - const resource = toResource(editor.input, { supportSideBySide: true }); - - // Support side-by-side binary editors too - let isBinaryEditor = false; - if (editor instanceof SideBySideEditor) { - isBinaryEditor = editor.getMasterEditor().getId() === BINARY_FILE_EDITOR_ID; - } else { - isBinaryEditor = editor.getId() === BINARY_FILE_EDITOR_ID; - } - - // Binary editor that should reload from event - if (resource && isBinaryEditor && (e.contains(resource, FileChangeType.UPDATED) || e.contains(resource, FileChangeType.ADDED))) { - this.editorService.openEditor(editor.input, { forceOpen: true, preserveFocus: true }, editor.group); - } - }); + // Handle updates to visible binary editors + this.handleUpdatesToVisibleBinaryEditors(e); } private handleUpdatesToTextModels(e: FileChangesEvent): void { @@ -332,10 +314,30 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // to have a size of 2 (1 running load and 1 queued load). const queue = this.modelLoadQueue.queueFor(model.getResource()); if (queue.size <= 1) { - queue.queue(() => model.load().then(null, errors.onUnexpectedError)); + queue.queue(() => model.load().then(null, onUnexpectedError)); } } + private handleUpdatesToVisibleBinaryEditors(e: FileChangesEvent): void { + const editors = this.editorService.visibleControls; + editors.forEach(editor => { + const resource = toResource(editor.input, { supportSideBySide: true }); + + // Support side-by-side binary editors too + let isBinaryEditor = false; + if (editor instanceof SideBySideEditor) { + isBinaryEditor = editor.getMasterEditor().getId() === BINARY_FILE_EDITOR_ID; + } else { + isBinaryEditor = editor.getId() === BINARY_FILE_EDITOR_ID; + } + + // Binary editor that should reload from event + if (resource && isBinaryEditor && (e.contains(resource, FileChangeType.UPDATED) || e.contains(resource, FileChangeType.ADDED))) { + this.editorService.openEditor(editor.input, { forceReload: true, preserveFocus: true }, editor.group); + } + }); + } + private handleOutOfWorkspaceWatchers(): void { const visibleOutOfWorkspacePaths = new ResourceMap(); this.editorService.visibleEditors.map(editorInput => { diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index f48ed0f4cf6..ee794e4861d 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -104,7 +104,7 @@ export class TextFileEditor extends BaseTextEditor { // Set input and resolve return super.setInput(input, options, token).then(() => { - return input.resolve(true).then(resolvedModel => { + return input.resolve().then(resolvedModel => { // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index df16421d173..29b277207b2 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -243,7 +243,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return this.forceOpenAsBinary ? BINARY_FILE_EDITOR_ID : TEXT_FILE_EDITOR_ID; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { // Resolve as binary if (this.forceOpenAsBinary) { @@ -251,13 +251,17 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } // Resolve as text - return this.doResolveAsText(refresh); + return this.doResolveAsText(); } - private doResolveAsText(reload?: boolean): TPromise { + private doResolveAsText(): TPromise { // Resolve as text - return this.textFileService.models.loadOrCreate(this.resource, { encoding: this.preferredEncoding, reload, allowBinary: this.forceOpenAsText }).then(model => { + return this.textFileService.models.loadOrCreate(this.resource, { + encoding: this.preferredEncoding, + reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model + allowBinary: this.forceOpenAsText + }).then(model => { // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index 57326f16477..257e5d35082 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -173,10 +173,10 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi this._register(this.contextService.onDidChangeWorkspaceName(e => this.updateTitleArea())); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); - - DOM.addClass(parent, 'explorer-viewlet'); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + DOM.addClass(parent, 'explorer-viewlet'); + }); } protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewletPanel { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 3b4181ae326..09615b2bae6 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -20,7 +20,7 @@ import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { isMacintosh, isLinux } from 'vs/base/common/platform'; import * as glob from 'vs/base/common/glob'; import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files'; @@ -56,6 +56,7 @@ import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class FileDataSource implements IDataSource { constructor( @@ -232,7 +233,7 @@ export class FileRenderer implements IRenderer { } public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileTemplateData { - const elementDisposable = EmptyDisposable; + const elementDisposable = Disposable.None; const label = this.instantiationService.createInstance(FileLabel, container, void 0); return { elementDisposable, label, container }; @@ -263,7 +264,7 @@ export class FileRenderer implements IRenderer { else { templateData.label.element.style.display = 'none'; this.renderInputBox(templateData.container, tree, stat, editableData); - templateData.elementDisposable = EmptyDisposable; + templateData.elementDisposable = Disposable.None; } } @@ -405,6 +406,7 @@ export class FileController extends WorkbenchTreeController implements IDisposab @IMenuService private menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IClipboardService private clipboardService: IClipboardService, + @IKeybindingService private keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService ) { super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService); @@ -537,6 +539,9 @@ export class FileController extends WorkbenchTreeController implements IDisposab tree.domFocus(); } }, + getKeyBinding: (action) => { + return this.keybindingService.lookupKeybinding(action.id); + }, getActionsContext: () => selection && selection.indexOf(stat) >= 0 ? selection.map((fs: ExplorerItem) => fs.resource) : stat instanceof ExplorerItem ? [stat.resource] : [] diff --git a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts index e56adb04c42..2355fbf8f1b 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts @@ -63,19 +63,19 @@ suite('Files - FileEditorInput', () => { const inputToResolve: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/file.js'), void 0); const sameOtherInput: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/file.js'), void 0); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert.ok(inputToResolve.isResolved()); const resolvedModelA = resolved; - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(resolvedModelA === resolved); // OK: Resolved Model cached globally per input - return sameOtherInput.resolve(true).then(otherResolved => { + return sameOtherInput.resolve().then(otherResolved => { assert(otherResolved === resolvedModelA); // OK: Resolved Model cached globally per input inputToResolve.dispose(); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(resolvedModelA === resolved); // Model is still the same because we had 2 clients inputToResolve.dispose(); @@ -83,17 +83,12 @@ suite('Files - FileEditorInput', () => { resolvedModelA.dispose(); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(resolvedModelA !== resolved); // Different instance, because input got disposed let stat = (resolved as TextFileEditorModel).getStat(); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(stat !== (resolved as TextFileEditorModel).getStat()); // Different stat, because resolve always goes to the server for refresh - - stat = (resolved as TextFileEditorModel).getStat(); - return inputToResolve.resolve(false).then(resolved => { - assert(stat === (resolved as TextFileEditorModel).getStat()); // Same stat, because not refreshed - }); }); }); }); @@ -122,7 +117,7 @@ suite('Files - FileEditorInput', () => { input.setEncoding('utf16', EncodingMode.Encode); assert.equal(input.getEncoding(), 'utf16'); - return input.resolve(true).then((resolved: TextFileEditorModel) => { + return input.resolve().then((resolved: TextFileEditorModel) => { assert.equal(input.getEncoding(), resolved.getEncoding()); resolved.dispose(); @@ -132,7 +127,7 @@ suite('Files - FileEditorInput', () => { test('save', function () { const input = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/updatefile.js'), void 0); - return input.resolve(true).then((resolved: TextFileEditorModel) => { + return input.resolve().then((resolved: TextFileEditorModel) => { resolved.textEditorModel.setValue('changed'); assert.ok(input.isDirty()); @@ -147,7 +142,7 @@ suite('Files - FileEditorInput', () => { test('revert', function () { const input = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/updatefile.js'), void 0); - return input.resolve(true).then((resolved: TextFileEditorModel) => { + return input.resolve().then((resolved: TextFileEditorModel) => { resolved.textEditorModel.setValue('changed'); assert.ok(input.isDirty()); @@ -164,7 +159,7 @@ suite('Files - FileEditorInput', () => { accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_IS_BINARY)); - return input.resolve(true).then(resolved => { + return input.resolve().then(resolved => { assert.ok(resolved); resolved.dispose(); @@ -176,7 +171,7 @@ suite('Files - FileEditorInput', () => { accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_TOO_LARGE)); - return input.resolve(true).then(resolved => { + return input.resolve().then(resolved => { assert.ok(resolved); resolved.dispose(); diff --git a/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts index 122d5047ffd..1729ad9165f 100644 --- a/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; -import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import { EditorOptions, EditorInput, IEditorMemento } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; @@ -42,8 +42,8 @@ export class HtmlPreviewPart extends BaseWebviewEditor { private _modelRef: IReference; public get model(): ITextModel { return this._modelRef && this._modelRef.object.textEditorModel; } - private _modelChangeSubscription = EmptyDisposable; - private _themeChangeSubscription = EmptyDisposable; + private _modelChangeSubscription = Disposable.None; + private _themeChangeSubscription = Disposable.None; private _content: HTMLElement; private _scrollYPercentage: number = 0; diff --git a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts index 2944b605a8c..340f821e0f4 100644 --- a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts +++ b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts @@ -141,9 +141,9 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo return; } - const currentLocale = this.getPossibleChineseMapping(locale); - const ceintlExtensionSearch = this.galleryService.query({ names: [`MS-CEINTL.vscode-language-pack-${currentLocale}`], pageSize: 1 }); - const tagSearch = this.galleryService.query({ text: `tag:lp-${currentLocale}`, pageSize: 1 }); + const extensionIdPostfix = this.getPossibleChineseMapping(locale); + const ceintlExtensionSearch = this.galleryService.query({ names: [`MS-CEINTL.vscode-language-pack-${extensionIdPostfix}`], pageSize: 1 }); + const tagSearch = this.galleryService.query({ text: `tag:lp-${locale}`, pageSize: 1 }); TPromise.join([ceintlExtensionSearch, tagSearch]).then(([ceintlResult, tagResult]) => { if (ceintlResult.total === 0 && tagResult.total === 0) { @@ -157,11 +157,11 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo return; } - TPromise.join([this.galleryService.getManifest(extensionToFetchTranslationsFrom), this.galleryService.getCoreTranslation(extensionToFetchTranslationsFrom, currentLocale)]) + TPromise.join([this.galleryService.getManifest(extensionToFetchTranslationsFrom), this.galleryService.getCoreTranslation(extensionToFetchTranslationsFrom, locale)]) .then(([manifest, translation]) => { - const loc = manifest && manifest.contributes && manifest.contributes.localizations && manifest.contributes.localizations.filter(x => this.getPossibleChineseMapping(x.languageId) === currentLocale)[0]; - const languageName = loc ? (loc.languageName || currentLocale) : currentLocale; - const languageDisplayName = loc ? (loc.localizedLanguageName || loc.languageName || currentLocale) : currentLocale; + const loc = manifest && manifest.contributes && manifest.contributes.localizations && manifest.contributes.localizations.filter(x => x.languageId.toLowerCase() === locale)[0]; + const languageName = loc ? (loc.languageName || locale) : locale; + const languageDisplayName = loc ? (loc.localizedLanguageName || loc.languageName || locale) : locale; const translationsFromPack = translation && translation.contents ? translation.contents['vs/platform/node/minimalTranslations'] : {}; const promptMessageKey = extensionToInstall ? 'installAndRestartMessage' : 'showLanguagePackExtensions'; const useEnglish = !translationsFromPack[promptMessageKey]; @@ -192,7 +192,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo this.viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) .then(viewlet => { - viewlet.search(`tag:lp-${currentLocale}`); + viewlet.search(`tag:lp-${locale}`); viewlet.focus(); }); } diff --git a/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts b/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts index d74ddb90984..7d22c8fc3a6 100644 --- a/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts +++ b/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts @@ -47,10 +47,7 @@ export class ConfigureLocaleAction extends Action { return undefined; } return this.editorService.openEditor({ - resource: stat.resource, - options: { - forceOpen: true - } + resource: stat.resource }); }, (error) => { throw new Error(localize('fail.createSettings', "Unable to create '{0}' ({1}).", getPathLabel(file, this.environmentService, this.contextService), error)); diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts index a5976612345..dde2adc6b77 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts @@ -50,6 +50,7 @@ export class MarkersPanel extends Panel { private treeContainer: HTMLElement; private messageBoxContainer: HTMLElement; + private ariaLabelElement: HTMLElement; private panelSettings: any; private currentResourceGotAddedToMarkersData: boolean = false; @@ -76,8 +77,9 @@ export class MarkersPanel extends Panel { dom.addClass(parent, 'markers-panel'); - let container = dom.append(parent, dom.$('.markers-panel-container')); + const container = dom.append(parent, dom.$('.markers-panel-container')); + this.createArialLabelElement(container); this.createMessageBox(container); this.createTree(container); this.createActions(); @@ -183,11 +185,17 @@ export class MarkersPanel extends Panel { private createMessageBox(parent: HTMLElement): void { this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container')); + this.messageBoxContainer.setAttribute('aria-labelledby', 'markers-panel-arialabel'); + } + + private createArialLabelElement(parent: HTMLElement): void { + this.ariaLabelElement = dom.append(parent, dom.$('')); + this.ariaLabelElement.setAttribute('id', 'markers-panel-arialabel'); + this.ariaLabelElement.setAttribute('aria-live', 'polite'); } private createTree(parent: HTMLElement): void { - this.treeContainer = dom.append(parent, dom.$('.tree-container')); - dom.addClass(this.treeContainer, 'show-file-icons'); + this.treeContainer = dom.append(parent, dom.$('.tree-container.show-file-icons')); const renderer = this.instantiationService.createInstance(Viewer.Renderer); const dnd = this.instantiationService.createInstance(SimpleFileResourceDragAndDrop, obj => obj instanceof ResourceMarkers ? obj.uri : void 0); const controller = this.instantiationService.createInstance(Controller); @@ -196,7 +204,7 @@ export class MarkersPanel extends Panel { filter: new Viewer.DataFilter(), renderer, controller, - accessibilityProvider: new Viewer.MarkersTreeAccessibilityProvider(), + accessibilityProvider: this.instantiationService.createInstance(Viewer.MarkersTreeAccessibilityProvider), dnd }, { twistiePixels: 20, @@ -291,11 +299,18 @@ export class MarkersPanel extends Panel { } private renderMessage(): void { - const markersModel = this.markersWorkbenchService.markersModel; - const hasFilteredResources = markersModel.hasFilteredResources(); dom.clearNode(this.messageBoxContainer); - dom.toggleClass(this.messageBoxContainer, 'hidden', hasFilteredResources); - if (!hasFilteredResources) { + const markersModel = this.markersWorkbenchService.markersModel; + if (markersModel.hasFilteredResources()) { + const { total, filtered } = markersModel.stats(); + if (filtered === total) { + this.ariaLabelElement.setAttribute('aria-label', localize('No problems filtered', "Showing {0} problems", total)); + } else { + this.ariaLabelElement.setAttribute('aria-label', localize('problems filtered', "Showing {0} of {1} problems", filtered, total)); + } + this.messageBoxContainer.removeAttribute('tabIndex'); + } else { + this.messageBoxContainer.setAttribute('tabIndex', '0'); if (markersModel.hasResources()) { if (markersModel.filterOptions.filter) { this.renderFilteredByFilterMessage(this.messageBoxContainer); @@ -315,6 +330,7 @@ export class MarkersPanel extends Panel { link.textContent = localize('disableFilesExclude', "Disable Files Exclude Filter."); link.setAttribute('tabIndex', '0'); dom.addDisposableListener(link, dom.EventType.CLICK, () => this.filterInputActionItem.useFilesExclude = false); + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILE_EXCLUSIONS_FILTER); } private renderFilteredByFilterMessage(container: HTMLElement) { @@ -324,11 +340,13 @@ export class MarkersPanel extends Panel { link.textContent = localize('clearFilter', "Clear Filter."); link.setAttribute('tabIndex', '0'); dom.addDisposableListener(link, dom.EventType.CLICK, () => this.filterInputActionItem.clear()); + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS); } private renderNoProblemsMessage(container: HTMLElement) { const span = dom.append(container, dom.$('span')); span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT; + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT); } private autoExpand(): void { diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts index b6479f0a8ec..fd918657fe9 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts @@ -93,13 +93,14 @@ export class MarkersFilterActionItem extends BaseActionItem { private delayedFilterUpdate: Delayer; private container: HTMLElement; + private element: HTMLElement; private filterInputBox: HistoryInputBox; private controlsContainer: HTMLInputElement; private filterBadge: HTMLInputElement; private filesExcludeFilter: Checkbox; constructor( - private itemOptions: IMarkersFilterActionItemOptions, + itemOptions: IMarkersFilterActionItemOptions, action: IAction, @IInstantiationService private instantiationService: IInstantiationService, @IContextViewService private contextViewService: IContextViewService, @@ -109,13 +110,13 @@ export class MarkersFilterActionItem extends BaseActionItem { ) { super(null, action); this.delayedFilterUpdate = new Delayer(500); + this.create(itemOptions); } render(container: HTMLElement): void { this.container = container; - DOM.addClass(this.container, 'markers-panel-action-filter'); - this.createInput(this.container); - this.createControls(this.container); + DOM.addClass(this.container, 'markers-panel-action-filter-container'); + DOM.append(this.container, this.element); this.adjustInputBox(); } @@ -124,7 +125,7 @@ export class MarkersFilterActionItem extends BaseActionItem { } getFilterText(): string { - return this.filterInputBox ? this.filterInputBox.value : this.itemOptions.filterText; + return this.filterInputBox.value; } getFilterHistory(): string[] { @@ -132,43 +133,47 @@ export class MarkersFilterActionItem extends BaseActionItem { } get useFilesExclude(): boolean { - return this.filesExcludeFilter ? this.filesExcludeFilter.checked : this.itemOptions.useFilesExclude; + return this.filesExcludeFilter.checked; } set useFilesExclude(useFilesExclude: boolean) { - if (this.filesExcludeFilter) { - if (this.filesExcludeFilter.checked !== useFilesExclude) { - this.filesExcludeFilter.checked = useFilesExclude; - this._onDidChange.fire(); - } + if (this.filesExcludeFilter.checked !== useFilesExclude) { + this.filesExcludeFilter.checked = useFilesExclude; + this._onDidChange.fire(); } } toggleLayout(small: boolean) { if (this.container) { DOM.toggleClass(this.container, 'small', small); - DOM.toggleClass(this.filterBadge, 'small', small); } } - private createInput(container: HTMLElement): void { + private create(itemOptions: IMarkersFilterActionItemOptions): void { + this.element = DOM.$('.markers-panel-action-filter'); + this.createInput(this.element, itemOptions); + this.createControls(this.element, itemOptions); + } + + private createInput(container: HTMLElement, itemOptions: IMarkersFilterActionItemOptions): void { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, - history: this.itemOptions.filterHistory + history: itemOptions.filterHistory })); + this.filterInputBox.inputElement.setAttribute('aria-labelledby', 'markers-panel-arialabel'); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); - this.filterInputBox.value = this.itemOptions.filterText; + this.filterInputBox.value = itemOptions.filterText; this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange()))); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, 'keydown', (keyboardEvent) => this.onInputKeyDown(keyboardEvent, this.filterInputBox))); this._register(DOM.addStandardDisposableListener(container, 'keydown', this.handleKeyboardEvent)); this._register(DOM.addStandardDisposableListener(container, 'keyup', this.handleKeyboardEvent)); } - private createControls(container: HTMLElement): void { + private createControls(container: HTMLElement, itemOptions: IMarkersFilterActionItemOptions): void { this.controlsContainer = DOM.append(container, DOM.$('.markers-panel-filter-controls')); this.createBadge(this.controlsContainer); - this.createFilesExcludeCheckbox(this.controlsContainer); + this.createFilesExcludeCheckbox(this.controlsContainer, itemOptions); } private createBadge(container: HTMLElement): void { @@ -187,11 +192,11 @@ export class MarkersFilterActionItem extends BaseActionItem { this._register(this.markersWorkbenchService.onDidChange(() => this.updateBadge())); } - private createFilesExcludeCheckbox(container: HTMLElement): void { + private createFilesExcludeCheckbox(container: HTMLElement, itemOptions: IMarkersFilterActionItemOptions): void { this.filesExcludeFilter = this._register(new Checkbox({ actionClassName: 'markers-panel-filter-filesExclude', - title: this.itemOptions.useFilesExclude ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE, - isChecked: this.itemOptions.useFilesExclude + title: itemOptions.useFilesExclude ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE, + isChecked: itemOptions.useFilesExclude })); this._register(this.filesExcludeFilter.onChange(() => { this.filesExcludeFilter.domNode.title = this.filesExcludeFilter.checked ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE; diff --git a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts index 5dee170ab08..b4eb6819d23 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts @@ -21,6 +21,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { getPathLabel } from 'vs/base/common/labels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; interface IResourceMarkersTemplateData { resourceLabel: ResourceLabel; @@ -256,9 +257,16 @@ export class Renderer implements IRenderer { export class MarkersTreeAccessibilityProvider implements IAccessibilityProvider { + constructor( + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IEnvironmentService private environmentService: IEnvironmentService + ) { + } + public getAriaLabel(tree: ITree, element: any): string { if (element instanceof ResourceMarkers) { - return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.name, element.filteredCount); + const path = getPathLabel(element.uri, this.environmentService, this.contextService) || element.uri.fsPath; + return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.filteredCount, element.name, paths.dirname(path)); } if (element instanceof Marker) { return Messages.MARKERS_TREE_ARIA_LABEL_MARKER(element); diff --git a/src/vs/workbench/parts/markers/electron-browser/media/markers.css b/src/vs/workbench/parts/markers/electron-browser/media/markers.css index 1aa1fecffe9..0d0649cfabc 100644 --- a/src/vs/workbench/parts/markers/electron-browser/media/markers.css +++ b/src/vs/workbench/parts/markers/electron-browser/media/markers.css @@ -3,30 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-action-bar .action-item.markers-panel-action-filter { +.monaco-action-bar .markers-panel-action-filter-container { cursor: default; margin-right: 10px; min-width: 150px; max-width: 500px; display: flex; - align-items: center; } -.monaco-action-bar .action-item.markers-panel-action-filter { +.monaco-action-bar .markers-panel-action-filter-container { flex: 0.7; } -.monaco-action-bar .action-item.markers-panel-action-filter.small { +.monaco-action-bar .markers-panel-action-filter-container.small { flex: 0.5; } -.monaco-action-bar .action-item.markers-panel-action-filter .monaco-inputbox { +.monaco-action-bar .markers-panel-action-filter { + display: flex; + align-items: center; + flex: 1; +} + +.monaco-action-bar .markers-panel-action-filter .monaco-inputbox { height: 24px; font-size: 12px; flex: 1; } -.vs .monaco-action-bar .action-item.markers-panel-action-filter .monaco-inputbox { +.vs .monaco-action-bar .markers-panel-action-filter .monaco-inputbox { height: 25px; border: 1px solid #ddd; } @@ -47,7 +52,7 @@ } .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.hidden, -.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.small { +.markers-panel-action-filter-container.small .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge { display: none; } @@ -79,8 +84,7 @@ display: none; } -.markers-panel .markers-panel-container .tree-container.hidden, -.markers-panel .markers-panel-container .message-box-container.hidden { +.markers-panel .markers-panel-container .tree-container.hidden { display: none; visibility: hidden; } diff --git a/src/vs/workbench/parts/markers/electron-browser/messages.ts b/src/vs/workbench/parts/markers/electron-browser/messages.ts index 7368df79ae4..f54ddaea508 100644 --- a/src/vs/workbench/parts/markers/electron-browser/messages.ts +++ b/src/vs/workbench/parts/markers/electron-browser/messages.ts @@ -45,7 +45,7 @@ export default class Messages { public static readonly MARKERS_PANEL_AT_LINE_COL_NUMBER = (ln: number, col: number): string => { return nls.localize('markers.panel.at.ln.col.number', "({0}, {1})", '' + ln, '' + col); }; - public static readonly MARKERS_TREE_ARIA_LABEL_RESOURCE = (fileName: string, noOfProblems: number): string => { return nls.localize('problems.tree.aria.label.resource', "{0} with {1} problems", fileName, noOfProblems); }; + public static readonly MARKERS_TREE_ARIA_LABEL_RESOURCE = (noOfProblems: number, fileName: string, folder: string): string => { return nls.localize('problems.tree.aria.label.resource', "{0} problems in file {1} of folder {2}", noOfProblems, fileName, folder); }; public static readonly MARKERS_TREE_ARIA_LABEL_MARKER = (marker: Marker): string => { const relatedInformationMessage = marker.resourceRelatedInformation.length ? nls.localize('problems.tree.aria.label.marker.relatedInformation', " This problem has references to {0} locations.", marker.resourceRelatedInformation.length) : ''; switch (marker.raw.severity) { diff --git a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css index 036064d3c6a..858ba8e3266 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css +++ b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css @@ -76,59 +76,6 @@ color: inherit !important; } -.monaco-workbench .outline-panel .outline-element { - display: flex; - flex: 1; - flex-flow: row nowrap; - align-items: center; -} - -.monaco-workbench .outline-panel .outline-element .outline-element-icon { - padding-right: 3px; -} - .monaco-workbench .outline-panel.no-icons .outline-element .outline-element-icon { display: none; } - -.monaco-workbench .outline-panel .outline-element .outline-element-label { - text-overflow: ellipsis; - overflow: hidden; - color: var(--outline-element-color); -} - -.monaco-workbench .outline-panel .outline-element .outline-element-label .monaco-highlighted-label .highlight { - font-weight: bold; -} - -.monaco-workbench .outline-panel .outline-element .outline-element-detail { - visibility: hidden; - flex: 1; - flex-basis: 10%; - opacity: 0.8; - overflow: hidden; - text-overflow: ellipsis; - font-size: 90%; - padding-left: 4px; - padding-top: 3px; -} - -.monaco-workbench .outline-panel .monaco-tree-row.focused .outline-element .outline-element-detail { - visibility: inherit; -} - -.monaco-workbench .outline-panel .outline-element .outline-element-decoration { - opacity: 0.75; - font-size: 90%; - font-weight: 600; - padding: 0 12px 0 5px; - margin-left: auto; - text-align: center; - color: var(--outline-element-color); -} - -.monaco-workbench .outline-panel .outline-element .outline-element-decoration.bubble { - font-family: octicons; - font-size: 14px; - opacity: 0.4; -} diff --git a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts index fa4a2ed4c07..66cbf82e5ae 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts +++ b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts @@ -13,8 +13,8 @@ import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Action, IAction, RadioGroup } from 'vs/base/common/actions'; import { firstIndex } from 'vs/base/common/arrays'; -import { asDisposablePromise, setDisposableTimeout } from 'vs/base/common/async'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { asDisposablePromise, setDisposableTimeout, createCancelablePromise } from 'vs/base/common/async'; +import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -54,6 +54,7 @@ import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron import { OutlineConfigKeys, OutlineViewFiltered, OutlineViewFocused, OutlineViewId } from './outline'; import { OutlineController, OutlineDataSource, OutlineItemComparator, OutlineItemCompareType, OutlineItemFilter, OutlineRenderer, OutlineTreeState } from '../../../../editor/contrib/documentSymbols/outlineTree'; import { IResourceInput } from 'vs/platform/editor/common/editor'; +import { ITextModel } from 'vs/editor/common/model'; class RequestState { @@ -135,11 +136,15 @@ class RequestOracle { let modeListener = codeEditor.onDidChangeModelLanguage(_ => { this._callback(codeEditor, undefined); }); + let disposeListener = codeEditor.onDidDispose(() => { + this._callback(undefined, undefined); + }); this._sessionDisposable = { dispose() { contentListener.dispose(); clearTimeout(handle); modeListener.dispose(); + disposeListener.dispose(); } }; } @@ -347,6 +352,9 @@ export class OutlinePanel extends ViewletPanel { return false; } const keyInfo = mapping[event.code]; + if (!keyInfo) { + return false; + } if (keyInfo.value) { $this._input.focus(); return true; @@ -440,6 +448,17 @@ export class OutlinePanel extends ViewletPanel { this._message.innerText = escape(message); } + private static _createOutlineModel(model: ITextModel, disposables: IDisposable[]): Promise { + let promise = createCancelablePromise(token => OutlineModel.create(model, token)); + disposables.push({ dispose() { promise.cancel(); } }); + return promise.catch(err => { + if (!isPromiseCanceledError(err)) { + throw err; + } + return undefined; + }); + } + private async _doUpdate(editor: ICodeEditor, event: IModelContentChangedEvent): Promise { dispose(this._editorDisposables); @@ -464,7 +483,7 @@ export class OutlinePanel extends ViewletPanel { ); } - let model = await asDisposablePromise(OutlineModel.create(textModel), undefined, this._editorDisposables).promise; + let model = await OutlinePanel._createOutlineModel(textModel, this._editorDisposables); dispose(loadingMessage); if (!model) { return; @@ -649,8 +668,7 @@ export class OutlinePanel extends ViewletPanel { options: { preserveFocus: !focus, selection: Range.collapseToStart(element.symbol.selectionRange), - revealInCenterIfOutsideViewport: true, - forceOpen: true + revealInCenterIfOutsideViewport: true } } as IResourceInput, aside ? SIDE_GROUP : ACTIVE_GROUP); } diff --git a/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts b/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts index 771485b80f1..8c55379ff49 100644 --- a/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts +++ b/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts @@ -7,4 +7,5 @@ import './startupProfiler'; import './startupTimings'; +import './startupTimingsAppender'; import './stats'; diff --git a/src/vs/workbench/parts/performance/electron-browser/startupTimingsAppender.ts b/src/vs/workbench/parts/performance/electron-browser/startupTimingsAppender.ts new file mode 100644 index 00000000000..2822fbb1890 --- /dev/null +++ b/src/vs/workbench/parts/performance/electron-browser/startupTimingsAppender.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ITimerService } from 'vs/workbench/services/timer/common/timerService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { nfcall } from 'vs/base/common/async'; +import { appendFile } from 'fs'; +import product from 'vs/platform/node/product'; + +class StartupTimingsAppender implements IWorkbenchContribution { + + constructor( + @ITimerService timerService: ITimerService, + @IWindowsService windowsService: IWindowsService, + @ILifecycleService lifecycleService: ILifecycleService, + @IExtensionService extensionService: IExtensionService, + @IEnvironmentService environmentService: IEnvironmentService, + ) { + + let appendTo = environmentService.args['prof-append-timers']; + if (!appendTo) { + return; + } + + Promise.all([ + lifecycleService.when(LifecyclePhase.Eventually), + extensionService.whenInstalledExtensionsRegistered() + ]).then(() => { + const { startupMetrics } = timerService; + return nfcall(appendFile, appendTo, `${product.nameShort}\t${product.commit || '0000000'}\t${Date.now()}\t${startupMetrics.ellapsed}\n`); + }).then(() => { + windowsService.quit(); + }).catch(err => { + console.error(err); + windowsService.quit(); + }); + } +} + +const registry = Registry.as(Extensions.Workbench); +registry.registerWorkbenchContribution(StartupTimingsAppender, LifecyclePhase.Running); diff --git a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts index ab0cfe703ed..9398a8c5046 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts @@ -214,7 +214,9 @@ export class DefineKeybindingWidget extends Widget { this._domNode.setClassName('defineKeybindingWidget'); this._domNode.setWidth(DefineKeybindingWidget.WIDTH); this._domNode.setHeight(DefineKeybindingWidget.HEIGHT); - dom.append(this._domNode.domNode, dom.$('.message', null, nls.localize('defineKeybinding.initial', "Press desired key combination and then press ENTER."))); + + const message = nls.localize('defineKeybinding.initial', "Press desired key combination and then press ENTER."); + dom.append(this._domNode.domNode, dom.$('.message', null, message)); this._register(attachStylerCallback(this.themeService, { editorWidgetBackground, widgetShadow }, colors => { if (colors.editorWidgetBackground) { @@ -230,7 +232,7 @@ export class DefineKeybindingWidget extends Widget { } })); - this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingInputWidget, this._domNode.domNode, {})); + this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingInputWidget, this._domNode.domNode, { ariaLabel: message })); this._register(this._keybindingInputWidget.onKeybinding(keybinding => this.onKeybinding(keybinding))); this._register(this._keybindingInputWidget.onEnter(() => this.hide())); this._register(this._keybindingInputWidget.onEscape(() => this.onCancel())); diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts index 4d91fd39cb5..2ef5827640a 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -74,6 +74,8 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor private searchFocusContextKey: IContextKey; private sortByPrecedence: Checkbox; + private ariaLabelElement: HTMLElement; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -100,6 +102,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor createEditor(parent: HTMLElement): void { const keybindingsEditorElement = DOM.append(parent, $('div', { class: 'keybindings-editor' })); + this.createAriaLabelElement(keybindingsEditorElement); this.createOverlayContainer(keybindingsEditorElement); this.createHeader(keybindingsEditorElement); this.createBody(keybindingsEditorElement); @@ -268,6 +271,12 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor return TPromise.as(null); } + private createAriaLabelElement(parent: HTMLElement): void { + this.ariaLabelElement = DOM.append(parent, DOM.$('')); + this.ariaLabelElement.setAttribute('id', 'keybindings-editor-aria-label-element'); + this.ariaLabelElement.setAttribute('aria-live', 'assertive'); + } + private createOverlayContainer(parent: HTMLElement): void { this.overlayContainer = DOM.append(parent, $('.overlay-container')); this.overlayContainer.style.position = 'absolute'; @@ -293,7 +302,8 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { ariaLabel: localize('SearchKeybindings.AriaLabel', "Search keybindings"), placeholder: localize('SearchKeybindings.Placeholder', "Search keybindings"), - focusKey: this.searchFocusContextKey + focusKey: this.searchFocusContextKey, + ariaLabelledBy: 'keybindings-editor-aria-label-element' })); this._register(this.searchWidget.onDidChange(searchValue => this.delayedFiltering.trigger(() => this.filterKeybindings()))); @@ -302,8 +312,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor isChecked: false, title: localize('sortByPrecedene', "Sort by Precedence") })); - this._register( - this.sortByPrecedence.onChange(() => this.renderKeybindingsEntries(false))); + this._register(this.sortByPrecedence.onChange(() => this.renderKeybindingsEntries(false))); searchContainer.appendChild(this.sortByPrecedence.domNode); this.createOpenKeybindingsElement(this.headerContainer); @@ -397,6 +406,9 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor if (this.keybindingsEditorModel) { const filter = this.searchWidget.getValue(); const keybindingsEntries: IKeybindingItemEntry[] = this.keybindingsEditorModel.fetch(filter, this.sortByPrecedence.checked); + + this.ariaLabelElement.setAttribute('aria-label', this.getAriaLabel(keybindingsEntries)); + if (keybindingsEntries.length === 0) { this.latestEmptyFilters.push(filter); } @@ -425,6 +437,14 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor } } + private getAriaLabel(keybindingsEntries: IKeybindingItemEntry[]): string { + if (this.sortByPrecedence.checked) { + return localize('show sorted keybindings', "Showing {0} Keybindings in precendence order", keybindingsEntries.length); + } else { + return localize('show keybindings', "Showing {0} Keybindings in alphabetical order", keybindingsEntries.length); + } + } + private layoutKebindingsList(): void { const listHeight = this.dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/); this.keybindingsListContainer.style.height = `${listHeight}px`; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index afaa9962fea..1875e5070fc 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -126,7 +126,8 @@ export class PreferencesEditor extends BaseEditor { ariaLabel: nls.localize('SearchSettingsWidget.AriaLabel', "Search settings"), placeholder: nls.localize('SearchSettingsWidget.Placeholder', "Search Settings"), focusKey: this.searchFocusContextKey, - showResultCount: true + showResultCount: true, + ariaLive: 'assertive' })); this._register(this.searchWidget.onDidChange(value => this.onInputChanged())); this._register(this.searchWidget.onFocus(() => this.lastFocusedWidget = this.searchWidget)); @@ -242,7 +243,7 @@ export class PreferencesEditor extends BaseEditor { if (query) { return TPromise.join([ this.localSearchDelayer.trigger(() => this.preferencesRenderers.localFilterPreferences(query)), - this.remoteSearchThrottle.trigger(() => this.progressService.showWhile(this.preferencesRenderers.remoteSearchPreferences(query), 500)) + this.remoteSearchThrottle.trigger(() => TPromise.wrap(this.progressService.showWhile(this.preferencesRenderers.remoteSearchPreferences(query), 500))) ]) as TPromise; } else { // When clearing the input, update immediately to clear it @@ -434,7 +435,7 @@ class PreferencesRenderersController extends Disposable { } } - private async _onEditableContentDidChange(): TPromise { + private async _onEditableContentDidChange(): Promise { await this.localFilterPreferences(this._lastQuery, true); await this.remoteSearchPreferences(this._lastQuery, true); } @@ -511,11 +512,11 @@ class PreferencesRenderersController extends Disposable { return TPromise.join(searchPs).then(() => { }); } - private searchSettingsTarget(query: string, provider: ISearchProvider, target: SettingsTarget, groupId: string, groupLabel: string, groupOrder: number): TPromise { + private searchSettingsTarget(query: string, provider: ISearchProvider, target: SettingsTarget, groupId: string, groupLabel: string, groupOrder: number): Promise { if (!query) { // Don't open the other settings targets when query is empty this._onDidFilterResultsCountChange.fire({ target, count: 0 }); - return TPromise.wrap(null); + return Promise.resolve(null); } return this.getPreferencesEditorModel(target).then(model => { @@ -532,7 +533,7 @@ class PreferencesRenderersController extends Disposable { }); } - private async getPreferencesEditorModel(target: SettingsTarget): TPromise { + private async getPreferencesEditorModel(target: SettingsTarget): Promise { const resource = target === ConfigurationTarget.USER ? this.preferencesService.userSettingsResource : target === ConfigurationTarget.WORKSPACE ? this.preferencesService.workspaceSettingsResource : target; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 723af7ffdc0..3c57b2f5fcb 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -561,6 +561,8 @@ export class SettingsTargetsWidget extends Widget { export interface SearchOptions extends IInputOptions { focusKey?: IContextKey; showResultCount?: boolean; + ariaLive?: string; + ariaLabelledBy?: string; } export class SearchWidget extends Widget { @@ -608,7 +610,10 @@ export class SearchWidget extends Widget { })); } - this.inputBox.inputElement.setAttribute('aria-live', 'assertive'); + this.inputBox.inputElement.setAttribute('aria-live', this.options.ariaLive || 'off'); + if (this.options.ariaLabelledBy) { + this.inputBox.inputElement.setAttribute('aria-labelledBy', this.options.ariaLabelledBy); + } const focusTracker = this._register(DOM.trackFocus(this.inputBox.inputElement)); this._register(focusTracker.onDidFocus(() => this._onFocus.fire())); diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index 82caff36780..9e11c49536a 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -189,7 +189,8 @@ export class SettingsEditor2 extends BaseEditor { this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"), placeholder: localize('SearchSettings.Placeholder', "Search settings"), - focusKey: this.searchFocusContextKey + focusKey: this.searchFocusContextKey, + ariaLive: 'assertive' })); this._register(this.searchWidget.onDidChange(() => this.onSearchInputChanged())); diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index e86b79e6e54..ac53257f46e 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -243,7 +243,9 @@ function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set): I return { id: tocData.id, label: tocData.label, - children: tocData.children.map(child => _resolveSettingsTree(child, allSettings)) + children: tocData.children + .map(child => _resolveSettingsTree(child, allSettings)) + .filter(child => (child.children && child.children.length) || (child.settings && child.settings.length)) }; } diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 3b40c62f656..f0037613a93 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -155,7 +155,7 @@ class RemoteSearchProvider implements ISearchProvider { @ILogService private logService: ILogService ) { this._remoteSearchP = this.options.filter ? - this.getSettingsForFilter(this.options.filter) : + TPromise.wrap(this.getSettingsForFilter(this.options.filter)) : TPromise.wrap(null); } @@ -196,7 +196,7 @@ class RemoteSearchProvider implements ISearchProvider { }); } - private async getSettingsForFilter(filter: string): TPromise { + private async getSettingsForFilter(filter: string): Promise { const allRequestDetails: IBingRequestDetails[] = []; // Only send MAX_REQUESTS requests in total just to keep it sane @@ -308,7 +308,7 @@ class RemoteSearchProvider implements ISearchProvider { }; } - private async prepareRequest(query: string, filterPage = 0): TPromise { + private async prepareRequest(query: string, filterPage = 0): Promise { const verbatimQuery = query; query = escapeSpecialChars(query); const boost = 10; @@ -526,4 +526,4 @@ class SettingMatches { endColumn: setting.valueRange.startColumn + match.end + 1 }; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts index 973f975cb0e..90a6893cdf4 100644 --- a/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts @@ -8,8 +8,8 @@ import * as nls from 'vs/nls'; import 'vs/css!./media/dirtydiffDecorator'; -import { ThrottledDelayer, always } from 'vs/base/common/async'; -import { IDisposable, dispose, toDisposable, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ThrottledDelayer, always, first } from 'vs/base/common/async'; +import { IDisposable, dispose, toDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter, anyEvent as anyEvent, filterEvent, once } from 'vs/base/common/event'; import * as ext from 'vs/workbench/common/contributions'; @@ -553,7 +553,7 @@ export class DirtyDiffController implements IEditorContribution { private widget: DirtyDiffWidget | null = null; private currentIndex: number = -1; private readonly isDirtyDiffVisible: IContextKey; - private session: IDisposable = EmptyDisposable; + private session: IDisposable = Disposable.None; private mouseDownInfo: { lineNumber: number } | null = null; private enabled = false; private disposables: IDisposable[] = []; @@ -611,7 +611,7 @@ export class DirtyDiffController implements IEditorContribution { close(): void { this.session.dispose(); - this.session = EmptyDisposable; + this.session = Disposable.None; } private assertWidget(): boolean { @@ -1032,20 +1032,13 @@ export class DirtyDiffModel { }); } - private async getOriginalResource(): TPromise { + private getOriginalResource(): TPromise { if (!this._editorModel) { - return null; + return TPromise.as(null); } - for (const repository of this.scmService.repositories) { - const result = repository.provider.getOriginalResource(this._editorModel.uri); - - if (result) { - return result; - } - } - - return null; + const uri = this._editorModel.uri; + return first(this.scmService.repositories.map(r => () => r.provider.getOriginalResource(uri))); } findNextClosestChange(lineNumber: number, inclusive = true): number { diff --git a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts index 9464225a74d..c0c4f11d15e 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/paths'; -import { IDisposable, dispose, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { filterEvent, anyEvent as anyEvent } from 'vs/base/common/event'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { ISCMService, ISCMRepository } from 'vs/workbench/services/scm/common/scm'; @@ -18,7 +18,7 @@ import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } export class StatusUpdater implements IWorkbenchContribution { - private badgeDisposable: IDisposable = EmptyDisposable; + private badgeDisposable: IDisposable = Disposable.None; private disposables: IDisposable[] = []; constructor( @@ -60,7 +60,7 @@ export class StatusUpdater implements IWorkbenchContribution { const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); this.badgeDisposable = this.activityService.showActivity(VIEWLET_ID, badge, 'scm-viewlet-label'); } else { - this.badgeDisposable = EmptyDisposable; + this.badgeDisposable = Disposable.None; } } @@ -72,8 +72,8 @@ export class StatusUpdater implements IWorkbenchContribution { export class StatusBarController implements IWorkbenchContribution { - private statusBarDisposable: IDisposable = EmptyDisposable; - private focusDisposable: IDisposable = EmptyDisposable; + private statusBarDisposable: IDisposable = Disposable.None; + private focusDisposable: IDisposable = Disposable.None; private focusedRepository: ISCMRepository | undefined = undefined; private focusedProviderContextKey: IContextKey; private disposables: IDisposable[] = []; diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 3a2eda194f7..b369614adb0 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -12,7 +12,7 @@ import { Event, Emitter, chain, mapEvent, anyEvent, filterEvent, latch } from 'v import { domEvent, stop } from 'vs/base/browser/event'; import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { PanelViewlet, ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, Dimension, addDisposableListener } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -142,7 +142,7 @@ class ProviderRenderer implements IRenderer new StatusBarActionItem(a as StatusBarAction) }); - const disposable = EmptyDisposable; + const disposable = Disposable.None; const templateDisposable = combinedDisposable([actionBar, badgeStyler]); return { title, type, countContainer, count, actionBar, disposable, templateDisposable }; @@ -389,7 +389,7 @@ class ResourceGroupRenderer implements IRenderer { @@ -501,7 +501,7 @@ class ResourceRenderer implements IRenderer { const decorationIcon = append(element, $('.decoration-icon')); return { - element, name, fileLabel, decorationIcon, actionBar, elementDisposable: EmptyDisposable, dispose: () => { + element, name, fileLabel, decorationIcon, actionBar, elementDisposable: Disposable.None, dispose: () => { actionBar.dispose(); fileLabel.dispose(); } @@ -811,12 +811,9 @@ export class RepositoryPanel extends ViewletPanel { this.inputBox.setPlaceHolder(placeholder); }; - const validationDelayer = new ThrottledDelayer(200); - + const validationDelayer = new ThrottledDelayer(200); const validate = () => { - validationDelayer.trigger(async (): TPromise => { - const result = await this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart); - + return this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart).then(result => { if (!result) { this.inputBox.inputElement.removeAttribute('aria-invalid'); this.inputBox.hideMessage(); @@ -827,15 +824,17 @@ export class RepositoryPanel extends ViewletPanel { }); }; + const triggerValidation = () => validationDelayer.trigger(validate); + this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { flexibleHeight: true }); this.disposables.push(attachInputBoxStyler(this.inputBox, this.themeService)); this.disposables.push(this.inputBox); - this.inputBox.onDidChange(validate, null, this.disposables); + this.inputBox.onDidChange(triggerValidation, null, this.disposables); const onKeyUp = domEvent(this.inputBox.inputElement, 'keyup'); const onMouseUp = domEvent(this.inputBox.inputElement, 'mouseup'); - anyEvent(onKeyUp, onMouseUp)(() => validate(), null, this.disposables); + anyEvent(onKeyUp, onMouseUp)(triggerValidation, null, this.disposables); this.inputBox.value = this.repository.input.value; this.inputBox.onDidChange(value => this.repository.input.value = value, null, this.disposables); @@ -1020,10 +1019,10 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle private menus: SCMMenus; private mainPanel: MainPanel | null = null; private cachedMainPanelHeight: number | undefined; - private mainPanelDisposable: IDisposable = EmptyDisposable; + private mainPanelDisposable: IDisposable = Disposable.None; private _repositories: ISCMRepository[] = []; private repositoryPanels: RepositoryPanel[] = []; - private singlePanelTitleActionsDisposable: IDisposable = EmptyDisposable; + private singlePanelTitleActionsDisposable: IDisposable = Disposable.None; private disposables: IDisposable[] = []; private lastFocusedRepository: ISCMRepository | undefined; @@ -1068,37 +1067,37 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle this.disposables.push(this.contributedViews); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + this.el = parent; + addClass(this.el, 'scm-viewlet'); + addClass(this.el, 'empty'); + append(parent, $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); - this.el = parent; - addClass(this.el, 'scm-viewlet'); - addClass(this.el, 'empty'); - append(parent, $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); + this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); + const onDidUpdateConfiguration = filterEvent(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowProviders')); + onDidUpdateConfiguration(this.onDidChangeRepositories, this, this.disposables); - const onDidUpdateConfiguration = filterEvent(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowProviders')); - onDidUpdateConfiguration(this.onDidChangeRepositories, this, this.disposables); + this.onDidChangeRepositories(); - this.onDidChangeRepositories(); + this.contributedViews.onDidAdd(this.onDidAddContributedViews, this, this.disposables); + this.contributedViews.onDidRemove(this.onDidRemoveContributedViews, this, this.disposables); - this.contributedViews.onDidAdd(this.onDidAddContributedViews, this, this.disposables); - this.contributedViews.onDidRemove(this.onDidRemoveContributedViews, this, this.disposables); + let index = this.getContributedViewsStartIndex(); + const contributedViews: IAddedViewDescriptorRef[] = this.contributedViews.visibleViewDescriptors.map(viewDescriptor => { + const size = this.contributedViews.getSize(viewDescriptor.id); + const collapsed = this.contributedViews.isCollapsed(viewDescriptor.id); + return { viewDescriptor, index: index++, size, collapsed }; + }); + if (contributedViews.length) { + this.onDidAddContributedViews(contributedViews); + } - let index = this.getContributedViewsStartIndex(); - const contributedViews: IAddedViewDescriptorRef[] = this.contributedViews.visibleViewDescriptors.map(viewDescriptor => { - const size = this.contributedViews.getSize(viewDescriptor.id); - const collapsed = this.contributedViews.isCollapsed(viewDescriptor.id); - return { viewDescriptor, index: index++, size, collapsed }; + this.onDidSashChange(this.saveContributedViewSizes, this, this.disposables); }); - if (contributedViews.length) { - this.onDidAddContributedViews(contributedViews); - } - - this.onDidSashChange(this.saveContributedViewSizes, this, this.disposables); } private onDidAddRepository(repository: ISCMRepository): void { @@ -1153,7 +1152,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle }); } else { this.mainPanelDisposable.dispose(); - this.mainPanelDisposable = EmptyDisposable; + this.mainPanelDisposable = Disposable.None; this.mainPanel = null; } } diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index 0fb3e737976..bd2aa0463a9 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -492,14 +492,15 @@ export class ReplaceAllInFolderAction extends AbstractSearchAndReplaceAction { super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), 'action-replace-all'); } - public async run(): TPromise { + public run(): TPromise { let nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, this.folderMatch); - await this.folderMatch.replaceAll(); - - if (nextFocusElement) { - this.viewer.setFocus(nextFocusElement); - } - this.viewer.domFocus(); + return this.folderMatch.replaceAll() + .then(() => { + if (nextFocusElement) { + this.viewer.setFocus(nextFocusElement); + } + this.viewer.domFocus(); + }); } } diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index 97c8695f834..50e7dadcfe5 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -13,13 +13,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Task, ContributedTask, CustomTask, TaskSet, TaskSorter, TaskEvent, TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskSummary, TaskTerminateResponse, TaskSystemInfo } from 'vs/workbench/parts/tasks/common/taskSystem'; +import { IStringDictionary } from 'vs/base/common/collections'; export { ITaskSummary, Task, TaskTerminateResponse }; export const ITaskService = createDecorator('taskService'); export interface ITaskProvider { - provideTasks(): TPromise; + provideTasks(validTypes: IStringDictionary): TPromise; } export interface RunOptions { 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 37c67511039..1f274236426 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -1062,8 +1062,8 @@ class TaskService implements ITaskService { this.editorService.openEditor({ resource, options: { - forceOpen: true, - pinned: false + pinned: false, + forceReload: true // because content might have changed } }); } @@ -1085,7 +1085,6 @@ class TaskService implements ITaskService { return this.editorService.openEditor({ resource, options: { - forceOpen: true, pinned: false } }).then(() => undefined); @@ -1299,7 +1298,9 @@ class TaskService implements ITaskService { } private getGroupedTasks(): TPromise { - return this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask').then(() => { + return TPromise.join([this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask'), TaskDefinitionRegistry.onReady()]).then(() => { + let validTypes: IStringDictionary = Object.create(null); + TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); return new TPromise((resolve, reject) => { let result: TaskSet[] = []; let counter: number = 0; @@ -1328,7 +1329,7 @@ class TaskService implements ITaskService { if (this.schemaVersion === JsonSchemaVersion.V2_0_0 && this._providers.size > 0) { this._providers.forEach((provider) => { counter++; - provider.provideTasks().done(done, error); + provider.provideTasks(validTypes).done(done, error); }); } else { resolve(result); @@ -2229,7 +2230,6 @@ class TaskService implements ITaskService { this.editorService.openEditor({ resource, options: { - forceOpen: true, pinned: configFileCreated // pin only if config file is created #8727 } }); @@ -2487,6 +2487,7 @@ let schema: IJSONSchema = { import schemaVersion1 from './jsonSchema_v1'; import schemaVersion2 from './jsonSchema_v2'; +import { TaskDefinitionRegistry } from 'vs/workbench/parts/tasks/common/taskDefinitionRegistry'; schema.definitions = { ...schemaVersion1.definitions, ...schemaVersion2.definitions, diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index bca473c50b7..81613ecef7b 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -70,6 +70,7 @@ export interface ITerminalConfiguration { windows: string[]; }; macOptionIsMeta: boolean; + macOptionClickForcesSelection: boolean; rendererType: 'auto' | 'canvas' | 'dom'; rightClickBehavior: 'default' | 'copyPaste' | 'selectWord'; cursorBlinking: boolean; diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css index 95d45b4670b..be7a3c7a177 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css @@ -162,4 +162,9 @@ .xterm-cursor-pointer { cursor: pointer !important; -} \ No newline at end of file +} + +.xterm.xterm-cursor-crosshair { + /* Column selection mode */ + cursor: crosshair !important; +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index d85a4c3c02a..97c1069a8c5 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -120,6 +120,11 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'default': false }, + 'terminal.integrated.macOptionClickForcesSelection': { + 'description': nls.localize('terminal.integrated.macOptionClickForcesSelection', "Whether to force selection when when using option+click on macOS, this will force a regular (line) selection and disallow the use of column selection mode. This enables copying and pasting using the regular terminal selection when in tmux mouse mode for example."), + 'type': 'boolean', + 'default': false + }, 'terminal.integrated.copyOnSelection': { 'description': nls.localize('terminal.integrated.copyOnSelection', "When set, text selected in the terminal will be copied to the clipboard."), 'type': 'boolean', diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 1a73d38dbd7..7d1a41951b3 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -65,7 +65,7 @@ export class TerminalInstance implements ITerminalInstance { private _rows: number; private _dimensionsOverride: ITerminalDimensions; private _windowsShellHelper: WindowsShellHelper; - private _xtermReadyPromise: TPromise; + private _xtermReadyPromise: Promise; private _disposables: lifecycle.IDisposable[]; private _messageTitleDisposable: lifecycle.IDisposable; @@ -205,7 +205,13 @@ export class TerminalInstance implements ITerminalInstance { // order to be precise. font.charWidth/charHeight alone as insufficient // when window.devicePixelRatio changes. const scaledWidthAvailable = dimension.width * window.devicePixelRatio; - const scaledCharWidth = Math.floor(font.charWidth * window.devicePixelRatio) + font.letterSpacing; + + let scaledCharWidth: number; + if (this._configHelper.config.rendererType === 'dom') { + scaledCharWidth = font.charWidth * window.devicePixelRatio; + } else { + scaledCharWidth = Math.floor(font.charWidth * window.devicePixelRatio) + font.letterSpacing; + } this._cols = Math.max(Math.floor(scaledWidthAvailable / scaledCharWidth), 1); const scaledHeightAvailable = dimension.height * window.devicePixelRatio; @@ -255,7 +261,7 @@ export class TerminalInstance implements ITerminalInstance { /** * Create xterm.js instance and attach data listeners. */ - protected async _createXterm(): TPromise { + protected async _createXterm(): Promise { if (!Terminal) { Terminal = (await import('vscode-xterm')).Terminal; // Enable xterm.js addons @@ -283,6 +289,7 @@ export class TerminalInstance implements ITerminalInstance { bellStyle: config.enableBell ? 'sound' : 'none', screenReaderMode: accessibilitySupport === 'on', macOptionIsMeta: config.macOptionIsMeta, + macOptionClickForcesSelection: config.macOptionClickForcesSelection, rightClickSelectsWord: config.rightClickBehavior === 'selectWord', // TODO: Guess whether to use canvas or dom better rendererType: config.rendererType === 'auto' ? 'canvas' : config.rendererType, @@ -871,6 +878,7 @@ export class TerminalInstance implements ITerminalInstance { this._setEnableBell(config.enableBell); this._safeSetOption('scrollback', config.scrollback); this._safeSetOption('macOptionIsMeta', config.macOptionIsMeta); + this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); } @@ -969,12 +977,15 @@ export class TerminalInstance implements ITerminalInstance { this._xterm.resize(cols, rows); if (this._isVisible) { - // Force the renderer to unpause by simulating an IntersectionObserver event. This - // is to fix an issue where dragging the window to the top of the screen to maximize - // on Winodws/Linux would fire an event saying that the terminal was not visible. - // This should only force a refresh if one is needed. + // HACK: Force the renderer to unpause by simulating an IntersectionObserver event. + // This is to fix an issue where dragging the window to the top of the screen to + // maximize on Windows/Linux would fire an event saying that the terminal was not + // visible. if (this._xterm.getOption('rendererType') === 'canvas') { this._xterm._core.renderer.onIntersectionChange({ intersectionRatio: 1 }); + // HACK: Force a refresh of the screen to ensure links are refresh corrected. + // This can probably be removed when the above hack is fixed in Chromium. + this._xterm.refresh(0, this._xterm.rows - 1); } } } diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index e9126845545..8987b358049 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -117,7 +117,8 @@ function cleanEnv() { 'PTYROWS', 'PTYSHELLCMDLINE', 'VSCODE_LOGS', - 'VSCODE_PORTABLE' + 'VSCODE_PORTABLE', + 'VSCODE_PID', ]; keys.forEach(function (key) { if (process.env[key]) { diff --git a/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts b/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts index bde131d66ed..4d2291134ef 100644 --- a/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts +++ b/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts @@ -77,13 +77,11 @@ export class SelectColorThemeAction extends Action { const placeHolder = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)"); const autoFocusIndex = firstIndex(picks, p => p.id === currentTheme.id); const delayer = new Delayer(100); + const chooseTheme = theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); + const tryTheme = theme => delayer.trigger(() => selectTheme(theme, false)); - return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex } }) - .then( - theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0), - null, - theme => delayer.trigger(() => selectTheme(theme, false)) - ); + return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex }, onDidFocus: tryTheme }) + .then(chooseTheme); }); } } @@ -136,13 +134,11 @@ class SelectIconThemeAction extends Action { const placeHolder = localize('themes.selectIconTheme', "Select File Icon Theme"); const autoFocusIndex = firstIndex(picks, p => p.id === currentTheme.id); const delayer = new Delayer(100); + const chooseTheme = theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); + const tryTheme = theme => delayer.trigger(() => selectTheme(theme, false)); - return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex } }) - .then( - theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0), - null, - theme => delayer.trigger(() => selectTheme(theme, false)) - ); + return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex }, onDidFocus: tryTheme }) + .then(chooseTheme); }); } } diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts index f1dcc32c086..b50ecda1765 100644 --- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts @@ -51,6 +51,7 @@ export class ReleaseNotesManager { private _releaseNotesCache: { [version: string]: TPromise; } = Object.create(null); private _currentReleaseNotes: WebviewEditorInput | undefined = undefined; + private _lastText: string; public constructor( @IEnvironmentService private readonly _environmentService: IEnvironmentService, @@ -60,14 +61,25 @@ export class ReleaseNotesManager { @IRequestService private readonly _requestService: IRequestService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IEditorService private readonly _editorService: IEditorService, - @IWebviewEditorService private readonly _webviewEditorService: IWebviewEditorService, - ) { } + @IWebviewEditorService private readonly _webviewEditorService: IWebviewEditorService + ) { + TokenizationRegistry.onDidChange(async () => { + if (!this._currentReleaseNotes || !this._lastText) { + return; + } + const html = await this.renderBody(this._lastText); + if (this._currentReleaseNotes) { + this._currentReleaseNotes.html = html; + } + }); + } public async show( accessor: ServicesAccessor, version: string - ): TPromise { + ): Promise { const releaseNoteText = await this.loadReleaseNotes(version); + this._lastText = releaseNoteText; const html = await this.renderBody(releaseNoteText); const title = nls.localize('releaseNotesInputName', "Release Notes: {0}", version); @@ -154,13 +166,14 @@ export class ReleaseNotesManager { } private async renderBody(text: string) { + const content = await this.renderContent(text); const colorMap = TokenizationRegistry.getColorMap(); const css = generateTokensCSSForColorMap(colorMap); - const body = renderBody(await this.renderContent(text), css); + const body = renderBody(content, css); return body; } - private async renderContent(text: string): TPromise { + private async renderContent(text: string): Promise { const renderer = await this.getRenderer(text); return marked(text, { renderer }); } diff --git a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts index 1a2d956f296..a87651e4bfc 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts @@ -13,15 +13,22 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IGlobalActivityRegistry, GlobalActivityExtensions } from 'vs/workbench/common/activity'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, Win3264BitContribution } from './update'; +import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, Win3264BitContribution, WinUserSetupContribution } from './update'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import product from 'vs/platform/node/product'; -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(ProductContribution, LifecyclePhase.Running); +const workbench = Registry.as(WorkbenchExtensions.Workbench); -if (platform.isWindows && process.arch === 'ia32') { - Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(Win3264BitContribution, LifecyclePhase.Running); +workbench.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Running); + +if (platform.isWindows) { + if (process.arch === 'ia32') { + workbench.registerWorkbenchContribution(Win3264BitContribution, LifecyclePhase.Running); + } + + if (product.target !== 'user') { + workbench.registerWorkbenchContribution(WinUserSetupContribution, LifecyclePhase.Running); + } } Registry.as(GlobalActivityExtensions) diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 2ae85c38570..110be30861b 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, Action } from 'vs/base/common/actions'; -import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; @@ -73,11 +73,11 @@ export abstract class AbstractShowReleaseNotesAction extends Action { this.enabled = false; - return showReleaseNotes(this.instantiationService, this.version) + return TPromise.wrap(showReleaseNotes(this.instantiationService, this.version) .then(null, () => { const action = this.instantiationService.createInstance(OpenLatestReleaseNotesInBrowserAction); return action.run().then(() => false); - }); + })); } } @@ -209,6 +209,60 @@ export class Win3264BitContribution implements IWorkbenchContribution { } } +export class WinUserSetupContribution implements IWorkbenchContribution { + + private static readonly KEY = 'update/win32-usersetup'; + + private static readonly STABLE_URL = 'https://vscode-update.azurewebsites.net/latest/win32-x64-user/stable'; + private static readonly STABLE_URL_32BIT = 'https://vscode-update.azurewebsites.net/latest/win32-user/stable'; + private static readonly INSIDER_URL = 'https://vscode-update.azurewebsites.net/latest/win32-x64-user/insider'; + private static readonly INSIDER_URL_32BIT = 'https://vscode-update.azurewebsites.net/latest/win32-user/insider'; + + // TODO@joao this needs to change to the 1.26 release notes + private static readonly READ_MORE = 'https://aka.ms/vscode-win32-user-setup'; + + constructor( + @IStorageService storageService: IStorageService, + @INotificationService notificationService: INotificationService, + @IEnvironmentService environmentService: IEnvironmentService, + @IOpenerService private openerService: IOpenerService + ) { + if (!environmentService.isBuilt || environmentService.disableUpdates) { + return; + } + + const neverShowAgain = new NeverShowAgain(WinUserSetupContribution.KEY, storageService); + + if (!neverShowAgain.shouldShow()) { + return; + } + + const handle = notificationService.prompt( + severity.Info, + nls.localize('usersetup', "We recommend switching to our new User Setup distribution of {0} for Windows! Click [here]({1}) to learn more.", product.nameShort, WinUserSetupContribution.READ_MORE), + [ + { + label: nls.localize('downloadnow', "Download"), + run: () => { + const url = product.quality === 'insider' + ? (process.arch === 'ia32' ? WinUserSetupContribution.INSIDER_URL_32BIT : WinUserSetupContribution.INSIDER_URL) + : (process.arch === 'ia32' ? WinUserSetupContribution.STABLE_URL_32BIT : WinUserSetupContribution.STABLE_URL); + + return this.openerService.open(URI.parse(url)); + } + }, + { + label: nls.localize('neveragain', "Don't Show Again"), + isSecondary: true, + run: () => { + neverShowAgain.action.run(handle); + neverShowAgain.action.dispose(); + } + }] + ); + } +} + class CommandAction extends Action { constructor( @@ -235,7 +289,7 @@ export class UpdateContribution implements IGlobalActivity { get cssClass() { return 'update-activity'; } private state: UpdateState; - private badgeDisposable: IDisposable = EmptyDisposable; + private badgeDisposable: IDisposable = Disposable.None; private disposables: IDisposable[] = []; constructor( @@ -507,4 +561,4 @@ export class UpdateContribution implements IGlobalActivity { dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/url/electron-browser/url.contribution.ts b/src/vs/workbench/parts/url/electron-browser/url.contribution.ts index bc6c1f8faa3..375cfdad0a4 100644 --- a/src/vs/workbench/parts/url/electron-browser/url.contribution.ts +++ b/src/vs/workbench/parts/url/electron-browser/url.contribution.ts @@ -27,11 +27,11 @@ export class OpenUrlAction extends Action { super(id, label); } - async run(): TPromise { - const input = await this.quickInputService.input({ prompt: 'URL to open' }); - const uri = URI.parse(input); - - this.urlService.open(uri); + run(): TPromise { + return this.quickInputService.input({ prompt: 'URL to open' }).then(input => { + const uri = URI.parse(input); + this.urlService.open(uri); + }); } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webview-pre.js b/src/vs/workbench/parts/webview/electron-browser/webview-pre.js index 51a9d42aa32..116392df7e9 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview-pre.js +++ b/src/vs/workbench/parts/webview/electron-browser/webview-pre.js @@ -172,7 +172,7 @@ } // apply default script - if (enableWrappedPostMessage) { + if (enableWrappedPostMessage && options.allowScripts) { const defaultScript = newDocument.createElement('script'); defaultScript.textContent = ` const acquireVsCodeApi = (function() { diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts index b4d98e02caf..82252859c77 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts @@ -8,7 +8,6 @@ import { domEvent } from 'vs/base/browser/event'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -163,22 +162,22 @@ export class WebviewEditor extends BaseWebviewEditor { super.clearInput(); } - async setInput(input: WebviewEditorInput, options: EditorOptions, token: CancellationToken): TPromise { + setInput(input: WebviewEditorInput, options: EditorOptions, token: CancellationToken): Thenable { if (this.input) { (this.input as WebviewEditorInput).releaseWebview(this); this._webview = undefined; this._webviewContent = undefined; } - await super.setInput(input, options, token); + return super.setInput(input, options, token) + .then(() => input.resolve()) + .then(() => { + if (token.isCancellationRequested) { + return; + } - await input.resolve(); - - if (token.isCancellationRequested) { - return; - } - - input.updateGroup(this.group.id); - this.updateWebview(input); + input.updateGroup(this.group.id); + this.updateWebview(input); + }); } private updateWebview(input: WebviewEditorInput) { diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts index 8a2a3f989bc..6d48bf36101 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts @@ -154,7 +154,7 @@ export class WebviewEditorInput extends EditorInput { } } - public resolve(refresh?: boolean): TPromise { + public resolve(): TPromise { if (this.reviver && !this._revived) { this._revived = true; return this.reviver.reviveWebview(this).then(() => new EditorModel()); diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts index 344a2153402..3c4228078b6 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts @@ -134,16 +134,18 @@ export class WebviewEditorService implements IWebviewEditorService { canRevive: (_webview) => { return true; }, - reviveWebview: async (webview: WebviewEditorInput): TPromise => { - const didRevive = await this.tryRevive(webview); - if (didRevive) { - return; - } - // A reviver may not be registered yet. Put into queue and resolve promise when we can revive - let resolve: (value: void) => void; - const promise = new TPromise(r => { resolve = r; }); - this._awaitingRevival.push({ input: webview, resolve }); - return promise; + reviveWebview: (webview: WebviewEditorInput): TPromise => { + return TPromise.wrap(this.tryRevive(webview)).then(didRevive => { + if (didRevive) { + return TPromise.as(void 0); + } + + // A reviver may not be registered yet. Put into queue and resolve promise when we can revive + let resolve: (value: void) => void; + const promise = new TPromise(r => { resolve = r; }); + this._awaitingRevival.push({ input: webview, resolve }); + return promise; + }); } }); @@ -183,7 +185,7 @@ export class WebviewEditorService implements IWebviewEditorService { private async tryRevive( webview: WebviewEditorInput - ): TPromise { + ): Promise { const revivers = this._revivers.get(webview.viewType); if (!revivers) { return false; @@ -197,4 +199,4 @@ export class WebviewEditorService implements IWebviewEditorService { } return false; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts index d5ee079299b..a97e8f8fd1c 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts @@ -131,71 +131,69 @@ export class WebviewElement extends Disposable { })); } - this.toDispose.push( - addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) { - console.log(`[Embedded Page] ${e.message}`); - }), - addDisposableListener(this._webview, 'dom-ready', () => { - this.layout(); - }), - addDisposableListener(this._webview, 'crashed', () => { - console.error('embedded page crashed'); - }), - addDisposableListener(this._webview, 'ipc-message', (event) => { - switch (event.channel) { - case 'onmessage': - if (this._options.enableWrappedPostMessage && event.args && event.args.length) { - this._onMessage.fire(event.args[0]); - } - return; + this._register(addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) { + console.log(`[Embedded Page] ${e.message}`); + })); + this._register(addDisposableListener(this._webview, 'dom-ready', () => { + this.layout(); + })); + this._register(addDisposableListener(this._webview, 'crashed', () => { + console.error('embedded page crashed'); + })); + this._register(addDisposableListener(this._webview, 'ipc-message', (event) => { + switch (event.channel) { + case 'onmessage': + if (this._options.enableWrappedPostMessage && event.args && event.args.length) { + this._onMessage.fire(event.args[0]); + } + return; - case 'did-click-link': - let [uri] = event.args; - this._onDidClickLink.fire(URI.parse(uri)); - return; + case 'did-click-link': + let [uri] = event.args; + this._onDidClickLink.fire(URI.parse(uri)); + return; - case 'did-set-content': - this._webview.style.flex = ''; - this._webview.style.width = '100%'; - this._webview.style.height = '100%'; - this.layout(); - return; + case 'did-set-content': + this._webview.style.flex = ''; + this._webview.style.width = '100%'; + this._webview.style.height = '100%'; + this.layout(); + return; - case 'did-scroll': - if (event.args && typeof event.args[0] === 'number') { - this._onDidScroll.fire({ scrollYPercentage: event.args[0] }); - } - return; + case 'did-scroll': + if (event.args && typeof event.args[0] === 'number') { + this._onDidScroll.fire({ scrollYPercentage: event.args[0] }); + } + return; - case 'do-reload': - this.reload(); - return; + case 'do-reload': + this.reload(); + return; - case 'do-update-state': - this._state = event.args[0]; - this._onDidUpdateState.fire(this._state); - return; - } - }), - addDisposableListener(this._webview, 'focus', () => { - if (this._contextKey) { - this._contextKey.set(true); - } - }), - addDisposableListener(this._webview, 'blur', () => { - if (this._contextKey) { - this._contextKey.reset(); - } - }), - addDisposableListener(this._webview, 'devtools-opened', () => { - this._send('devtools-opened'); - }), - ); + case 'do-update-state': + this._state = event.args[0]; + this._onDidUpdateState.fire(this._state); + return; + } + })); + this._register(addDisposableListener(this._webview, 'focus', () => { + if (this._contextKey) { + this._contextKey.set(true); + } + })); + this._register(addDisposableListener(this._webview, 'blur', () => { + if (this._contextKey) { + this._contextKey.reset(); + } + })); + this._register(addDisposableListener(this._webview, 'devtools-opened', () => { + this._send('devtools-opened'); + })); this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this.style(this._themeService.getTheme()); - this._themeService.onThemeChange(this.style, this, this.toDispose); + this._register(this._themeService.onThemeChange(this.style, this)); } public mountTo(parent: HTMLElement) { diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts index 67102680756..e9860c97d85 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -38,6 +38,7 @@ import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifi import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { TimeoutTimer } from 'vs/base/common/async'; used(); @@ -431,10 +432,10 @@ class WelcomePage { [{ label: localize('ok', "OK"), run: () => { - const messageDelay = TPromise.timeout(300); - messageDelay.then(() => { + const messageDelay = new TimeoutTimer(); + messageDelay.cancelAndSet(() => { this.notificationService.info(strings.installing.replace('{0}', extensionSuggestion.name)); - }); + }, 300); TPromise.join(extensionSuggestion.isKeymap ? extensions.filter(extension => isKeymapExtension(this.tipsService, extension) && extension.globallyEnabled) .map(extension => { return this.extensionEnablementService.setEnablement(extension.local, EnablementState.Disabled); diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md index b2fd54b2e19..26477b37563 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md @@ -103,7 +103,7 @@ function findFirstEvenNumber(arr) { ### Formatting -Keeping your code looking great is hard without a good formatter. Luckily it's easy to format content either the entire document with kb(editor.action.formatDocument). Formatting can be applied to the current selection with kb(editor.action.formatSelection). Both of these options are also available through the right-click context menu. +Keeping your code looking great is hard without a good formatter. Luckily it's easy to format content, either for the entire document with kb(editor.action.formatDocument) or for the current selection with kb(editor.action.formatSelection). Both of these options are also available through the right-click context menu. ```js var cars = ["Saab", "Volvo", "BMW"]; diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts index dd6a525c2fd..0c8665ff148 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts @@ -259,7 +259,7 @@ export class WalkThroughPart extends BaseEditor { return super.setInput(input, options, token) .then(() => { - return input.resolve(true); + return input.resolve(); }) .then(model => { if (token.isCancellationRequested) { diff --git a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts index 81b269fa5e6..2a9d7dae0c9 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts @@ -102,7 +102,7 @@ export class WalkThroughInput extends EditorInput { return this.options.onReady; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { if (!this.promise) { this.promise = this.textModelResolverService.createModelReference(this.options.resource) .then(ref => { diff --git a/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts index 7b1ca552400..a23fcba984b 100644 --- a/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts @@ -343,7 +343,7 @@ export class BulkEdit { } else if (edit.newUri && !edit.oldUri) { let ignoreIfExists = edit.options && edit.options.ignoreIfExists; if (!ignoreIfExists || !await this._fileService.existsFile(edit.newUri)) { - await this._fileService.createFile(edit.newUri, undefined, { overwrite }); + await this._textFileService.create(edit.newUri, undefined, { overwrite }); } } } diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 0d0fda5d2c1..68d7dc962d3 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -8,7 +8,7 @@ import { equals } from 'vs/base/common/objects'; import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationChangeEvent, ConfigurationModel, AbstractConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { ResourceMap } from 'vs/base/common/map'; @@ -101,18 +101,27 @@ export class FolderSettingsModelParser extends ConfigurationModelParser { } private parseWorkspaceSettings(rawSettings: any): void { - const rawWorkspaceSettings = {}; const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - for (let key in rawSettings) { - const scope = this.getScope(key, configurationProperties); - if (this.scopes.indexOf(scope) !== -1) { - rawWorkspaceSettings[key] = rawSettings[key]; - } - } + const rawWorkspaceSettings = this.filterByScope(rawSettings, configurationProperties, true); const configurationModel = this.parseRaw(rawWorkspaceSettings); this._settingsModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides); } + private filterByScope(properties: {}, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean): {} { + const result = {}; + for (let key in properties) { + if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) { + result[key] = this.filterByScope(properties[key], configurationProperties, false); + } else { + const scope = this.getScope(key, configurationProperties); + if (this.scopes.indexOf(scope) !== -1) { + result[key] = properties[key]; + } + } + } + return result; + } + private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { const propertySchema = configurationProperties[key]; return propertySchema ? propertySchema.scope : ConfigurationScope.WINDOW; diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index a47582c4722..e09d62ad085 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -284,7 +284,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura } }).then(null, err => [] /* never fail this call */); - return bulkContentFetchromise.then(() => TPromise.join(workspaceFilePathToConfiguration).then(result => collections.values(result))); + return bulkContentFetchromise.then(() => TPromise.join(collections.values(workspaceFilePathToConfiguration))); } private handleWorkspaceFileEvents(event: FileChangesEvent): void { @@ -489,4 +489,4 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat } return TPromise.as(null); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 99e62f15189..1aafc56a10f 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -80,7 +80,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this._register(this.userConfiguration.onDidChangeConfiguration(() => this.onUserConfigurationChanged())); this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged())); - this._register(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(e => this.registerConfigurationSchemas())); + this._register(Registry.as(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas())); this._register(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); this.workspaceEditingQueue = new Queue(); @@ -295,9 +295,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this._configuration.keys(); } - initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise { + initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration, postInitialisationTask: () => void = () => null): TPromise { return this.createWorkspace(arg) - .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace)); + .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask)); } acquireFileService(fileService: IFileService): void { @@ -373,7 +373,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return TPromise.as(new Workspace(id)); } - private updateWorkspaceAndInitializeConfiguration(workspace: Workspace): TPromise { + private updateWorkspaceAndInitializeConfiguration(workspace: Workspace, postInitialisationTask: () => void): TPromise { const hasWorkspaceBefore = !!this.workspace; let previousState: WorkbenchState; let previousWorkspacePath: string; @@ -389,6 +389,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat } return this.initializeConfiguration().then(() => { + + postInitialisationTask(); // Post initialisation task should be run before triggering events. + // Trigger changes after configuration initialization so that configuration is up to date. if (hasWorkspaceBefore) { const newState = this.getWorkbenchState(); diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 2a397b9df49..ec8107bed48 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -30,7 +30,8 @@ suite('FolderSettingsModelParser', () => { 'FolderSettingsModelParser.resource': { 'type': 'string', 'default': 'isSet', - scope: ConfigurationScope.RESOURCE + scope: ConfigurationScope.RESOURCE, + overridable: true }, 'FolderSettingsModelParser.application': { 'type': 'string', @@ -57,6 +58,14 @@ suite('FolderSettingsModelParser', () => { assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } }); }); + test('parse overridable resource settings', () => { + const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]); + + testObject.parse(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' } })); + + assert.deepEqual(testObject.configurationModel.overrides, [{ 'contents': { 'FolderSettingsModelParser': { 'resource': 'resource' } }, 'identifiers': ['json'] }]); + }); + test('reprocess folder settings excludes application setting', () => { const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); diff --git a/src/vs/workbench/services/decorations/browser/decorations.ts b/src/vs/workbench/services/decorations/browser/decorations.ts index e777db83119..6d1e6607375 100644 --- a/src/vs/workbench/services/decorations/browser/decorations.ts +++ b/src/vs/workbench/services/decorations/browser/decorations.ts @@ -9,7 +9,7 @@ import URI from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const IDecorationsService = createDecorator('IFileDecorationsService'); @@ -32,7 +32,7 @@ export interface IDecoration { export interface IDecorationsProvider { readonly label: string; readonly onDidChange: Event; - provideDecorations(uri: URI): IDecorationData | TPromise; + provideDecorations(uri: URI, token: CancellationToken): IDecorationData | Thenable; } export interface IResourceDecorationChangeEvent { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 513955a5b04..146bafc26a3 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -17,8 +17,8 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; import { IIterator } from 'vs/base/common/iterator'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; class DecorationRule { @@ -180,7 +180,7 @@ class DecorationStyles { let usedDecorations = new Set(); for (let e = iter.next(); !e.done; e = iter.next()) { e.value.data.forEach((value, key) => { - if (!isThenable(value) && value) { + if (value && !(value instanceof DecorationDataRequest)) { usedDecorations.add(DecorationRule.keyOf(value)); } }); @@ -229,9 +229,16 @@ class FileDecorationChangeEvent implements IResourceDecorationChangeEvent { } } +class DecorationDataRequest { + constructor( + readonly source: CancellationTokenSource, + readonly thenable: Thenable, + ) { } +} + class DecorationProviderWrapper { - readonly data = TernarySearchTree.forPaths | IDecorationData>(); + readonly data = TernarySearchTree.forPaths(); private readonly _dispoable: IDisposable; constructor( @@ -275,7 +282,7 @@ class DecorationProviderWrapper { item = this._fetchData(uri); } - if (item && !isThenable(item)) { + if (item && !(item instanceof DecorationDataRequest)) { // found something (which isn't pending anymore) callback(item, false); } @@ -285,7 +292,7 @@ class DecorationProviderWrapper { const iter = this.data.findSuperstr(key); if (iter) { for (let item = iter.next(); !item.done; item = iter.next()) { - if (item.value && !isThenable(item.value)) { + if (item.value && !(item.value instanceof DecorationDataRequest)) { callback(item.value, true); } } @@ -297,27 +304,28 @@ class DecorationProviderWrapper { // check for pending request and cancel it const pendingRequest = this.data.get(uri.toString()); - if (TPromise.is(pendingRequest)) { - pendingRequest.cancel(); + if (pendingRequest instanceof DecorationDataRequest) { + pendingRequest.source.cancel(); this.data.delete(uri.toString()); } - const dataOrThenable = this._provider.provideDecorations(uri); + const source = new CancellationTokenSource(); + const dataOrThenable = this._provider.provideDecorations(uri, source.token); if (!isThenable(dataOrThenable)) { // sync -> we have a result now return this._keepItem(uri, dataOrThenable); } else { // async -> we have a result soon - const request = TPromise.wrap(dataOrThenable).then(data => { + const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => { if (this.data.get(uri.toString()) === request) { this._keepItem(uri, data); } - }, err => { + }).catch(err => { if (!isPromiseCanceledError(err) && this.data.get(uri.toString()) === request) { this.data.delete(uri.toString()); } - }); + })); this.data.set(uri.toString(), request); return undefined; diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index f8469b69e23..c94ec82944a 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -12,6 +12,7 @@ import URI from 'vs/base/common/uri'; import { Event, toPromise, Emitter } from 'vs/base/common/event'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; suite('DecorationsService', function () { @@ -34,7 +35,7 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return new TPromise(resolve => { + return new Promise(resolve => { setTimeout(() => resolve({ color: 'someBlue', tooltip: 'T' @@ -174,6 +175,7 @@ suite('DecorationsService', function () { test('Decorations not showing up for second root folder #48502', async function () { let cancelCount = 0; + let winjsCancelCount = 0; let callCount = 0; let provider = new class implements IDecorationsProvider { @@ -183,14 +185,19 @@ suite('DecorationsService', function () { label: string = 'foo'; - provideDecorations(uri: URI): TPromise { + provideDecorations(uri: URI, token: CancellationToken): TPromise { + + token.onCancellationRequested(() => { + cancelCount += 1; + }); + return new TPromise(resolve => { callCount += 1; setTimeout(() => { resolve({ letter: 'foo' }); }, 10); }, () => { - cancelCount += 1; + winjsCancelCount += 1; }); } }; @@ -204,6 +211,7 @@ suite('DecorationsService', function () { service.getDecoration(uri, false); assert.equal(cancelCount, 1); + assert.equal(winjsCancelCount, 0); assert.equal(callCount, 2); reg.dispose(); @@ -219,7 +227,7 @@ suite('DecorationsService', function () { if (uri.path.match(/hello$/)) { return { tooltip: 'FOO', weight: 17, bubble: true }; } else { - return new TPromise(_resolve => resolve = _resolve); + return new Promise(_resolve => resolve = _resolve); } } }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index f5c38e6dd49..41138949711 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -494,7 +494,7 @@ suite('Editor service', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - editor = await service.openEditor(input, { forceOpen: true }); + editor = await service.openEditor(input, { forceReload: true }); assertActiveEditorChangedEvent(false); assertVisibleEditorsChangedEvent(false); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index c1b98e04a52..c2f0c78bfe2 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -649,7 +649,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } } - private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): TPromise { + private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise { const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); @@ -684,7 +684,7 @@ export class ExtensionService extends Disposable implements IExtensionService { ); } - private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): TPromise { + private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): Promise { const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); @@ -698,7 +698,7 @@ export class ExtensionService extends Disposable implements IExtensionService { return null; } - private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): TPromise { + private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): Promise { const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); @@ -715,7 +715,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } } - private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise { + private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise { if (input.devMode) { // Do not cache when running out of sources... return ExtensionScanner.scanExtensions(input, log); @@ -788,7 +788,7 @@ export class ExtensionService extends Disposable implements IExtensionService { log ); - let finalBuiltinExtensions: TPromise = builtinExtensions; + let finalBuiltinExtensions: TPromise = TPromise.wrap(builtinExtensions); if (devMode) { const builtInExtensionsFilePath = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'build', 'builtInExtensions.json')); diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 8de75c2f9a6..90cfe42920d 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -208,7 +208,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { * Parses original message bundle, returns null if the original message bundle is null. */ private static resolveOriginalMessageBundle(originalMessageBundle: string, errors: json.ParseError[]) { - return new TPromise<{ [key: string]: string; }>((c, e, p) => { + return new TPromise<{ [key: string]: string; }>((c, e) => { if (originalMessageBundle) { pfs.readFile(originalMessageBundle).then(originalBundleContent => { c(json.parse(originalBundleContent.toString(), errors)); @@ -226,7 +226,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { * If the localized file is not present, returns null for the original and marks original as localized. */ private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string): TPromise<{ localized: string, original: string }> { - return new TPromise<{ localized: string, original: string }>((c, e, p) => { + return new TPromise<{ localized: string, original: string }>((c, e) => { function loop(basename: string, locale: string): void { let toCheck = `${basename}.nls.${locale}.json`; pfs.fileExists(toCheck).then(exists => { @@ -524,7 +524,7 @@ export class ExtensionScanner { /** * Scan a list of extensions defined in `absoluteFolderPath` */ - public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): TPromise { + public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): Promise { const absoluteFolderPath = input.absoluteFolderPath; const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts index b11e518b965..98691d9a030 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -9,11 +9,12 @@ import * as path from 'path'; import * as platform from 'vs/base/common/platform'; import * as watcher from 'vs/workbench/services/files/node/watcher/common'; import * as nsfw from 'vscode-nsfw'; -import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; -import { TPromise, ProgressCallback, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; +import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; +import { TPromise, TValueCallback } from 'vs/base/common/winjs.base'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; import { normalizeNFC } from 'vs/base/common/normalization'; +import { Event, Emitter } from 'vs/base/common/event'; const nsfwActionToRawChangeType: { [key: number]: number } = []; nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED; @@ -35,20 +36,15 @@ export class NsfwWatcherService implements IWatcherService { private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private _pathWatchers: { [watchPath: string]: IPathWatcher } = {}; - private _watcherPromise: TPromise; - private _progressCallback: ProgressCallback; - private _errorCallback: ErrorCallback; private _verboseLogging: boolean; private enospcErrorLogged: boolean; - public initialize(verboseLogging: boolean): TPromise { - this._verboseLogging = true; - this._watcherPromise = new TPromise((c, e, p) => { - this._errorCallback = e; - this._progressCallback = p; + private _onWatchEvent = new Emitter(); + readonly onWatchEvent = this._onWatchEvent.event; - }); - return this._watcherPromise; + watch(options: IWatcherOptions): Event { + this._verboseLogging = options.verboseLogging; + return this.onWatchEvent; } private _watch(request: IWatcherRequest): void { @@ -70,7 +66,7 @@ export class NsfwWatcherService implements IWatcherService { // See https://github.com/Microsoft/vscode/issues/7950 if (e === 'Inotify limit reached' && !this.enospcErrorLogged) { this.enospcErrorLogged = true; - this._errorCallback(new Error('Inotify limit reached (ENOSPC)')); + this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' }); } }); @@ -119,7 +115,7 @@ export class NsfwWatcherService implements IWatcherService { // Broadcast to clients normalized const res = watcher.normalize(events); - this._progressCallback(res); + this._onWatchEvent.fire(res); // Logging if (this._verboseLogging) { diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts index 7e571c74ea4..771933f3559 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts @@ -6,13 +6,23 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherRequest { basePath: string; ignored: string[]; } +export interface IWatcherOptions { + verboseLogging: boolean; +} + +export interface IWatchError { + message: string; +} + export interface IWatcherService { - initialize(verboseLogging: boolean): TPromise; + watch(options: IWatcherOptions): Event; setRoots(roots: IWatcherRequest[]): TPromise; } \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts index 88ebc3d19da..1502478f301 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts @@ -7,21 +7,31 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWatcherRequest, IWatcherService } from './watcher'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherChannel extends IChannel { - call(command: 'initialize', verboseLogging: boolean): TPromise; + listen(event: 'watch', verboseLogging: boolean): Event; + listen(event: string, arg?: any): Event; + call(command: 'setRoots', request: IWatcherRequest[]): TPromise; - call(command: string, arg: any): TPromise; + call(command: string, arg?: any): TPromise; } export class WatcherChannel implements IWatcherChannel { constructor(private service: IWatcherService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'watch': return this.service.watch(arg); + } + throw new Error('No events'); + } + call(command: string, arg: any): TPromise { switch (command) { - case 'initialize': return this.service.initialize(arg); case 'setRoots': return this.service.setRoots(arg); } return undefined; @@ -32,8 +42,8 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IWatcherChannel) { } - initialize(verboseLogging: boolean): TPromise { - return this.channel.call('initialize', verboseLogging); + watch(options: IWatcherOptions): Event { + return this.channel.listen('watch', options); } setRoots(roots: IWatcherRequest[]): TPromise { diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts index e5403578de2..d2791142347 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts @@ -5,7 +5,6 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import uri from 'vs/base/common/uri'; @@ -16,7 +15,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { filterEvent } from 'vs/base/common/event'; +import { IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; export class FileWatcher { private static readonly MAX_RESTARTS = 5; @@ -24,7 +24,7 @@ export class FileWatcher { private service: WatcherChannelClient; private isDisposed: boolean; private restartCounter: number; - private toDispose: IDisposable[]; + private toDispose: IDisposable[] = []; constructor( private contextService: IWorkspaceContextService, @@ -35,17 +35,14 @@ export class FileWatcher { ) { this.isDisposed = false; this.restartCounter = 0; - this.toDispose = []; } public startWatching(): () => void { - const args = ['--type=watcherService']; - const client = new Client( uri.parse(require.toUrl('bootstrap')).fsPath, { serverName: 'Watcher', - args, + args: ['--type=watcherService'], env: { AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp', PIPE_LOGGING: 'true', @@ -55,16 +52,7 @@ export class FileWatcher { ); this.toDispose.push(client); - // Initialize watcher - const channel = getNextTickChannel(client.getChannel('watcher')); - this.service = new WatcherChannelClient(channel); - this.service.initialize(this.verboseLogging).then(null, err => { - if (!this.isDisposed && !isPromiseCanceledError(err)) { - return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up - } - return void 0; - }, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => { - + client.onDidProcessExit(() => { // our watcher app should never be completed because it keeps on watching. being in here indicates // that the watcher process died and we want to restart it here. we only do it a max number of times if (!this.isDisposed) { @@ -76,11 +64,20 @@ export class FileWatcher { this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!'); } } - }, error => { - if (!this.isDisposed) { - this.errorLogger(error); - } - }); + }, null, this.toDispose); + + // Initialize watcher + const channel = getNextTickChannel(client.getChannel('watcher')); + this.service = new WatcherChannelClient(channel); + + const options = { verboseLogging: this.verboseLogging }; + const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed); + + const onError = filterEvent(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); + onError(err => this.errorLogger(err.message), null, this.toDispose); + + const onFileChanges = filterEvent(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0); + onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose); // Start watching this.updateFolders(); @@ -118,17 +115,6 @@ export class FileWatcher { })); } - private onRawFileEvents(events: IRawFileChange[]): void { - if (this.isDisposed) { - return; - } - - // Emit through event emitter - if (events.length > 0) { - this.onFileChanges(toFileChangesEvent(events)); - } - } - private dispose(): void { this.isDisposed = true; this.toDispose = dispose(this.toDispose); diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts index 550297d06f0..ec8c491949c 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts @@ -21,7 +21,8 @@ import { normalizeNFC } from 'vs/base/common/normalization'; import { realcaseSync } from 'vs/base/node/extfs'; import { isMacintosh } from 'vs/base/common/platform'; import * as watcherCommon from 'vs/workbench/services/files/node/watcher/common'; -import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { Emitter, Event } from 'vs/base/common/event'; interface IWatcher { requests: ExtendedWatcherRequest[]; @@ -44,29 +45,20 @@ export class ChokidarWatcherService implements IWatcherService { private _watchers: { [watchPath: string]: IWatcher }; private _watcherCount: number; - private _watcherPromise: TPromise; private _options: IWatcherOptions & IChockidarWatcherOptions; private spamCheckStartTime: number; private spamWarningLogged: boolean; private enospcErrorLogged: boolean; - private _errorCallback: (error: Error) => void; - private _fileChangeCallback: (changes: watcherCommon.IRawFileChange[]) => void; - public initialize(options: IWatcherOptions & IChockidarWatcherOptions): TPromise { + private _onWatchEvent = new Emitter(); + readonly onWatchEvent = this._onWatchEvent.event; + + watch(options: IWatcherOptions & IChockidarWatcherOptions): Event { this._options = options; this._watchers = Object.create(null); this._watcherCount = 0; - this._watcherPromise = new TPromise((c, e, p) => { - this._errorCallback = (error) => { - this.stop(); - e(error); - }; - this._fileChangeCallback = p; - }, () => { - this.stop(); - }); - return this._watcherPromise; + return this.onWatchEvent; } public setRoots(requests: IWatcherRequest[]): TPromise { @@ -233,7 +225,7 @@ export class ChokidarWatcherService implements IWatcherService { // Broadcast to clients normalized const res = watcherCommon.normalize(events); - this._fileChangeCallback(res); + this._onWatchEvent.fire(res); // Logging if (this._options.verboseLogging) { @@ -257,7 +249,8 @@ export class ChokidarWatcherService implements IWatcherService { if ((error).code === 'ENOSPC') { if (!this.enospcErrorLogged) { this.enospcErrorLogged = true; - this._errorCallback(new Error('Inotify limit reached (ENOSPC)')); + this.stop(); + this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' }); } } else { console.error(error.toString()); diff --git a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts index 9f9ce6309c7..ef26ad06b85 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -137,19 +137,15 @@ suite.skip('Chockidar watching', () => { await pfs.mkdirp(bFolder); await pfs.mkdirp(b2Folder); - const promise = service.initialize({ verboseLogging: false, pollingInterval: 200 }); - promise.then(null, - e => { - console.log('set error', e); - error = e; - }, - p => { - if (Array.isArray(p)) { - result.push(...p); - } + const opts = { verboseLogging: false, pollingInterval: 200 }; + service.watch(opts)(e => { + if (Array.isArray(e)) { + result.push(...e); + } else { + console.log('set error', e.message); + error = e.message; } - ); - + }); }); suiteTeardown(async () => { diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts b/src/vs/workbench/services/files/node/watcher/unix/watcher.ts index 8ed35e6c792..771933f3559 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcher.ts @@ -6,6 +6,8 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherRequest { basePath: string; @@ -16,7 +18,11 @@ export interface IWatcherOptions { verboseLogging: boolean; } -export interface IWatcherService { - initialize(options: IWatcherOptions): TPromise; - setRoots(roots: IWatcherRequest[]): TPromise; +export interface IWatchError { + message: string; } + +export interface IWatcherService { + watch(options: IWatcherOptions): Event; + setRoots(roots: IWatcherRequest[]): TPromise; +} \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts index bf2fba4664b..1502478f301 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts @@ -7,21 +7,31 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherChannel extends IChannel { - call(command: 'initialize', options: IWatcherOptions): TPromise; + listen(event: 'watch', verboseLogging: boolean): Event; + listen(event: string, arg?: any): Event; + call(command: 'setRoots', request: IWatcherRequest[]): TPromise; - call(command: string, arg: any): TPromise; + call(command: string, arg?: any): TPromise; } export class WatcherChannel implements IWatcherChannel { constructor(private service: IWatcherService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'watch': return this.service.watch(arg); + } + throw new Error('No events'); + } + call(command: string, arg: any): TPromise { switch (command) { - case 'initialize': return this.service.initialize(arg); case 'setRoots': return this.service.setRoots(arg); } return undefined; @@ -32,8 +42,8 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IWatcherChannel) { } - initialize(options: IWatcherOptions): TPromise { - return this.channel.call('initialize', options); + watch(options: IWatcherOptions): Event { + return this.channel.listen('watch', options); } setRoots(roots: IWatcherRequest[]): TPromise { diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts index e51c287854f..45ca79e02f5 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts @@ -5,7 +5,6 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import uri from 'vs/base/common/uri'; @@ -13,10 +12,11 @@ import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/ import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc'; import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Schemas } from 'vs/base/common/network'; +import { filterEvent } from 'vs/base/common/event'; +import { IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; export class FileWatcher { private static readonly MAX_RESTARTS = 5; @@ -55,20 +55,7 @@ export class FileWatcher { ); this.toDispose.push(client); - const channel = getNextTickChannel(client.getChannel('watcher')); - this.service = new WatcherChannelClient(channel); - - const options = { - verboseLogging: this.verboseLogging - }; - - this.service.initialize(options).then(null, err => { - if (!this.isDisposed && !isPromiseCanceledError(err)) { - return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up - } - return void 0; - }, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => { - + client.onDidProcessExit(() => { // our watcher app should never be completed because it keeps on watching. being in here indicates // that the watcher process died and we want to restart it here. we only do it a max number of times if (!this.isDisposed) { @@ -80,11 +67,19 @@ export class FileWatcher { this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!'); } } - }, error => { - if (!this.isDisposed) { - this.errorLogger(error); - } - }); + }, null, this.toDispose); + + const channel = getNextTickChannel(client.getChannel('watcher')); + this.service = new WatcherChannelClient(channel); + + const options = { verboseLogging: this.verboseLogging }; + const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed); + + const onError = filterEvent(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); + onError(err => this.errorLogger(err.message), null, this.toDispose); + + const onFileChanges = filterEvent(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0); + onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose); // Start watching this.updateFolders(); @@ -123,17 +118,6 @@ export class FileWatcher { })); } - private onRawFileEvents(events: IRawFileChange[]): void { - if (this.isDisposed) { - return; - } - - // Emit through event emitter - if (events.length > 0) { - this.onFileChanges(toFileChangesEvent(events)); - } - } - private dispose(): void { this.isDisposed = true; this.toDispose = dispose(this.toDispose); diff --git a/src/vs/workbench/services/group/common/editorGroupsService.ts b/src/vs/workbench/services/group/common/editorGroupsService.ts index dc1595f47d1..e744ba63b0f 100644 --- a/src/vs/workbench/services/group/common/editorGroupsService.ts +++ b/src/vs/workbench/services/group/common/editorGroupsService.ts @@ -312,6 +312,13 @@ export interface IGroupChangeEvent { editorIndex?: number; } +export interface IEditorBreadcrumbs { + focus(): void; + focusNext(): void; + focusPrev(): void; + select(): void; +} + export interface IEditorGroup { /** @@ -332,6 +339,11 @@ export interface IEditorGroup { */ readonly label: string; + /** + * + */ + readonly breadcrumbs: IEditorBreadcrumbs; + /** * The active control is the currently visible control of the group. */ @@ -476,4 +488,4 @@ export interface IEditorGroup { * Invoke a function in the context of the services of this group. */ invokeWithinContext(fn: (accessor: ServicesAccessor) => T): T; -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index b81ca559188..045707197af 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -70,7 +70,7 @@ export class KeybindingsEditorInput extends EditorInput { return nls.localize('keybindingsInputName', "Keyboard Shortcuts"); } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(this.keybindingsModel); } @@ -97,7 +97,7 @@ export class SettingsEditor2Input extends EditorInput { return nls.localize('settingsEditor2InputName', "Settings (Preview)"); } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return >this.preferencesService.createPreferencesEditorModel(URI.parse('vscode://defaultsettings/0/settings.json')); } diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index df20cc78925..9a1eacf76a6 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -15,7 +15,7 @@ import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { TPromise } from 'vs/base/common/winjs.base'; -import { always } from 'vs/base/common/async'; +import { always, timeout } from 'vs/base/common/async'; import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity'; import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification'; import { Action } from 'vs/base/common/actions'; @@ -130,8 +130,8 @@ export class ProgressService2 implements IProgressService2 { this._updateWindowProgress(); // show progress for at least 150ms - always(TPromise.join([ - TPromise.timeout(150), + always(Promise.all([ + timeout(150), promise ]), () => { const idx = this._stack.indexOf(task); @@ -142,7 +142,7 @@ export class ProgressService2 implements IProgressService2 { }, 150); // cancel delay if promise finishes below 150ms - always(TPromise.wrap(promise), () => clearTimeout(delayHandle)); + always(promise, () => clearTimeout(delayHandle)); return promise; } @@ -270,7 +270,7 @@ export class ProgressService2 implements IProgressService2 { }); // Show progress for at least 800ms and then hide once done or canceled - always(TPromise.join([TPromise.timeout(800), p]), () => { + always(Promise.all([timeout(800), p]), () => { if (handle) { handle.close(); } diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 1182c79e506..dcfb3c3db8e 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -25,7 +25,7 @@ import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/searc import * as extfs from 'vs/base/node/extfs'; import * as flow from 'vs/base/node/flow'; -import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search'; +import { IRawFileMatch, IRawSearch, ISearchEngine, IFolderSearch, ISerializedSearchSuccess } from './search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { rgErrorMsgForDisplay } from './ripgrepTextSearch'; @@ -721,9 +721,10 @@ export class Engine implements ISearchEngine { this.walker = new FileWalker(config); } - public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void { this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => { done(err, { + type: 'success', limitHit: isLimitHit, stats: this.walker.getStats() }); diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index fd40541b99c..d267e0cd3a0 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -11,7 +11,7 @@ import { join, sep } from 'path'; import * as arrays from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import * as strings from 'vs/base/common/strings'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search'; @@ -19,10 +19,14 @@ import { Engine as FileSearchEngine, FileWalker } from 'vs/workbench/services/se import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch'; import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch'; import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider'; -import { IFileSearchProgressItem, IRawFileMatch, IRawSearch, IRawSearchService, ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent } from './search'; +import { IFileSearchProgressItem, IRawFileMatch, IRawSearch, IRawSearchService, ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent, ISerializedSearchSuccess } from './search'; +import { Event, Emitter } from 'vs/base/common/event'; gracefulFs.gracefulify(fs); +type IProgressCallback = (p: ISerializedSearchProgressItem) => void; +type IFileProgressCallback = (p: IFileSearchProgressItem) => void; + export class SearchService implements IRawSearchService { private static readonly BATCH_SIZE = 512; @@ -31,29 +35,52 @@ export class SearchService implements IRawSearchService { private textSearchWorkerProvider: TextSearchWorkerProvider; - private telemetryPipe: (event: ITelemetryEvent) => void; + private _onTelemetry = new Emitter(); + readonly onTelemetry: Event = this._onTelemetry.event; - public fileSearch(config: IRawSearch): PPromise { - return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE); + public fileSearch(config: IRawSearch, batchSize = SearchService.BATCH_SIZE): Event { + let promise: TPromise; + + const emitter = new Emitter({ + onFirstListenerAdd: () => { + promise = this.doFileSearch(FileSearchEngine, config, p => emitter.fire(p), batchSize) + .then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err })); + }, + onLastListenerRemove: () => { + promise.cancel(); + } + }); + + return emitter.event; } - public textSearch(config: IRawSearch): PPromise { - return config.useRipgrep ? - this.ripgrepTextSearch(config) : - this.legacyTextSearch(config); + public textSearch(config: IRawSearch): Event { + let promise: TPromise; + + const emitter = new Emitter({ + onFirstListenerAdd: () => { + promise = (config.useRipgrep ? this.ripgrepTextSearch(config, p => emitter.fire(p)) : this.legacyTextSearch(config, p => emitter.fire(p))) + .then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err })); + }, + onLastListenerRemove: () => { + promise.cancel(); + } + }); + + return emitter.event; } - public ripgrepTextSearch(config: IRawSearch): PPromise { + private ripgrepTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise { config.maxFilesize = MAX_FILE_SIZE; let engine = new RipgrepEngine(config); - return new PPromise((c, e, p) => { + return new TPromise((c, e) => { // Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned - const collector = new BatchedCollector(SearchService.BATCH_SIZE, p); + const collector = new BatchedCollector(SearchService.BATCH_SIZE, progressCallback); engine.search((match) => { collector.addItem(match, match.numMatches); }, (message) => { - p(message); + progressCallback(message); }, (error, stats) => { collector.flush(); @@ -68,7 +95,7 @@ export class SearchService implements IRawSearchService { }); } - public legacyTextSearch(config: IRawSearch): PPromise { + private legacyTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise { if (!this.textSearchWorkerProvider) { this.textSearchWorkerProvider = new TextSearchWorkerProvider(); } @@ -86,75 +113,75 @@ export class SearchService implements IRawSearchService { }), this.textSearchWorkerProvider); - return this.doTextSearch(engine, SearchService.BATCH_SIZE); + return this.doTextSearch(engine, progressCallback, SearchService.BATCH_SIZE); } - public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine; }, config: IRawSearch, batchSize?: number): PPromise { + doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine; }, config: IRawSearch, progressCallback: IProgressCallback, batchSize?: number): TPromise { + const fileProgressCallback: IFileProgressCallback = progress => { + if (Array.isArray(progress)) { + progressCallback(progress.map(m => this.rawMatchToSearchItem(m))); + } else if ((progress).relativePath) { + progressCallback(this.rawMatchToSearchItem(progress)); + } else { + progressCallback(progress); + } + }; if (config.sortByScore) { - let sortedSearch = this.trySortedSearchFromCache(config); + let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback); if (!sortedSearch) { const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config; const engine = new EngineClass(walkerConfig); - sortedSearch = this.doSortedSearch(engine, config); + sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback); } - return new PPromise((c, e, p) => { + return new TPromise((c, e) => { process.nextTick(() => { // allow caller to register progress callback first sortedSearch.then(([result, rawMatches]) => { const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch)); - this.sendProgress(serializedMatches, p, batchSize); + this.sendProgress(serializedMatches, progressCallback, batchSize); c(result); - }, e, p); + }, e); }); }, () => { sortedSearch.cancel(); }); } - let searchPromise: PPromise; - return new PPromise((c, e, p) => { - const engine = new EngineClass(config); - searchPromise = this.doSearch(engine, batchSize) - .then(c, e, progress => { - if (Array.isArray(progress)) { - p(progress.map(m => this.rawMatchToSearchItem(m))); - } else if ((progress).relativePath) { - p(this.rawMatchToSearchItem(progress)); - } else { - p(progress); - } - }); - }, () => { - searchPromise.cancel(); - }); + const engine = new EngineClass(config); + + return this.doSearch(engine, fileProgressCallback, batchSize); } private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch { return { path: match.base ? join(match.base, match.relativePath) : match.relativePath }; } - private doSortedSearch(engine: ISearchEngine, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> { - let searchPromise: PPromise; - let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => { + private doSortedSearch(engine: ISearchEngine, config: IRawSearch, progressCallback: IProgressCallback, fileProgressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> { + let searchPromise: TPromise; + const emitter = new Emitter(); + + let allResultsPromise = new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => { let results: IRawFileMatch[] = []; - searchPromise = this.doSearch(engine, -1) + + const innerProgressCallback: IFileProgressCallback = progress => { + if (Array.isArray(progress)) { + results = progress; + } else { + fileProgressCallback(progress); + emitter.fire(progress); + } + }; + + searchPromise = this.doSearch(engine, innerProgressCallback, -1) .then(result => { c([result, results]); - if (this.telemetryPipe) { - // __GDPR__TODO__ classify event - this.telemetryPipe({ - eventName: 'fileSearch', - data: result.stats - }); - } - }, e, progress => { - if (Array.isArray(progress)) { - results = progress; - } else { - p(progress); - } - }); + // __GDPR__TODO__ classify event + this._onTelemetry.fire({ + eventName: 'fileSearch', + data: result.stats + }); + }, e); }, () => { searchPromise.cancel(); }); @@ -162,7 +189,10 @@ export class SearchService implements IRawSearchService { let cache: Cache; if (config.cacheKey) { cache = this.getOrCreateCache(config.cacheKey); - cache.resultsToSearchCache[config.filePattern] = allResultsPromise; + cache.resultsToSearchCache[config.filePattern] = { + promise: allResultsPromise, + event: emitter.event + }; allResultsPromise.then(null, err => { delete cache.resultsToSearchCache[config.filePattern]; }); @@ -170,7 +200,7 @@ export class SearchService implements IRawSearchService { } let chained: TPromise; - return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => { + return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => { chained = allResultsPromise.then(([result, results]) => { const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); const unsortedResultTime = Date.now(); @@ -179,14 +209,15 @@ export class SearchService implements IRawSearchService { const sortedResultTime = Date.now(); c([{ + type: 'success', stats: objects.assign({}, result.stats, { unsortedResultTime, sortedResultTime }), limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults - }, sortedResults]); + } as ISerializedSearchSuccess, sortedResults]); }); - }, e, p); + }, e); }, () => { chained.cancel(); }); @@ -200,17 +231,17 @@ export class SearchService implements IRawSearchService { return this.caches[cacheKey] = new Cache(); } - private trySortedSearchFromCache(config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> { + private trySortedSearchFromCache(config: IRawSearch, progressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> { const cache = config.cacheKey && this.caches[config.cacheKey]; if (!cache) { return undefined; } const cacheLookupStartTime = Date.now(); - const cached = this.getResultsFromCache(cache, config.filePattern); + const cached = this.getResultsFromCache(cache, config.filePattern, progressCallback); if (cached) { let chained: TPromise; - return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => { + return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => { chained = cached.then(([result, results, cacheStats]) => { const cacheLookupResultTime = Date.now(); return this.sortResults(config, results, cache.scorerCache) @@ -234,13 +265,14 @@ export class SearchService implements IRawSearchService { } c([ { + type: 'success', limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults, stats: stats - }, + } as ISerializedSearchSuccess, sortedResults ]); }); - }, e, p); + }, e); }, () => { chained.cancel(); }); @@ -259,7 +291,7 @@ export class SearchService implements IRawSearchService { return arrays.topAsync(results, compare, config.maxResults, 10000); } - private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize: number) { + private sendProgress(results: ISerializedFileMatch[], progressCb: IProgressCallback, batchSize: number) { if (batchSize && batchSize > 0) { for (let i = 0; i < results.length; i += batchSize) { progressCb(results.slice(i, i + batchSize)); @@ -269,10 +301,10 @@ export class SearchService implements IRawSearchService { } } - private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress> { + private getResultsFromCache(cache: Cache, searchValue: string, progressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[], CacheStats]> { // Find cache entries by prefix of search value const hasPathSep = searchValue.indexOf(sep) >= 0; - let cached: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; + let cachedRow: CacheRow; let wasResolved: boolean; for (let previousSearch in cache.resultsToSearchCache) { @@ -282,20 +314,25 @@ export class SearchService implements IRawSearchService { continue; // since a path character widens the search for potential more matches, require it in previous search too } - const c = cache.resultsToSearchCache[previousSearch]; - c.then(() => { wasResolved = false; }); + const row = cache.resultsToSearchCache[previousSearch]; + row.promise.then(() => { wasResolved = false; }); wasResolved = true; - cached = this.preventCancellation(c); + cachedRow = { + promise: this.preventCancellation(row.promise), + event: row.event + }; break; } } - if (!cached) { + if (!cachedRow) { return null; } - return new PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress>((c, e, p) => { - cached.then(([complete, cachedEntries]) => { + const listener = cachedRow.event(progressCallback); + + return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[], CacheStats]>((c, e) => { + cachedRow.promise.then(([complete, cachedEntries]) => { const cacheFilterStartTime = Date.now(); // Pattern match on results @@ -317,21 +354,22 @@ export class SearchService implements IRawSearchService { cacheFilterStartTime: cacheFilterStartTime, cacheFilterResultCount: cachedEntries.length }]); - }, e, p); + }, e); }, () => { - cached.cancel(); + cachedRow.promise.cancel(); + listener.dispose(); }); } - private doTextSearch(engine: TextSearchEngine, batchSize: number): PPromise { - return new PPromise((c, e, p) => { + private doTextSearch(engine: TextSearchEngine, progressCallback: IProgressCallback, batchSize: number): TPromise { + return new TPromise((c, e) => { // Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned - const collector = new BatchedCollector(batchSize, p); + const collector = new BatchedCollector(batchSize, progressCallback); engine.search((matches) => { const totalMatches = matches.reduce((acc, m) => acc + m.numMatches, 0); collector.addItems(matches, totalMatches); }, (progress) => { - p(progress); + progressCallback(progress); }, (error, stats) => { collector.flush(); @@ -346,28 +384,28 @@ export class SearchService implements IRawSearchService { }); } - private doSearch(engine: ISearchEngine, batchSize?: number): PPromise { - return new PPromise((c, e, p) => { + private doSearch(engine: ISearchEngine, progressCallback: IFileProgressCallback, batchSize?: number): TPromise { + return new TPromise((c, e) => { let batch: IRawFileMatch[] = []; engine.search((match) => { if (match) { if (batchSize) { batch.push(match); if (batchSize > 0 && batch.length >= batchSize) { - p(batch); + progressCallback(batch); batch = []; } } else { - p(match); + progressCallback(match); } } }, (progress) => { process.nextTick(() => { - p(progress); + progressCallback(progress); }); }, (error, stats) => { if (batch.length) { - p(batch); + progressCallback(batch); } if (error) { e(error); @@ -385,19 +423,11 @@ export class SearchService implements IRawSearchService { return TPromise.as(undefined); } - public fetchTelemetry(): PPromise { - return new PPromise((c, e, p) => { - this.telemetryPipe = p; - }, () => { - this.telemetryPipe = null; - }); - } - - private preventCancellation(promise: PPromise): PPromise { - return new PPromise((c, e, p) => { + private preventCancellation(promise: TPromise): TPromise { + return new TPromise((c, e) => { // Allow for piled up cancellations to come through first. process.nextTick(() => { - promise.then(c, e, p); + promise.then(c, e); }); }, () => { // Do not propagate. @@ -405,9 +435,14 @@ export class SearchService implements IRawSearchService { } } +interface CacheRow { + promise: TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>; + event: Event; +} + class Cache { - public resultsToSearchCache: { [searchValue: string]: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; } = Object.create(null); + public resultsToSearchCache: { [searchValue: string]: CacheRow; } = Object.create(null); public scorerCache: ScorerCache = Object.create(null); } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts index c42d0659d93..81898c0880d 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts @@ -18,7 +18,7 @@ import * as encoding from 'vs/base/node/encoding'; import * as extfs from 'vs/base/node/extfs'; import { IProgress } from 'vs/platform/search/common/search'; import { rgPath } from 'vscode-ripgrep'; -import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, ISerializedSearchComplete, LineMatch } from './search'; +import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, LineMatch, ISerializedSearchSuccess } from './search'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); @@ -44,10 +44,11 @@ export class RipgrepEngine { } // TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore - search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void { if (!this.config.folderQueries.length && !this.config.extraFiles.length) { process.removeListener('exit', this.killRgProcFn); done(null, { + type: 'success', limitHit: false, stats: null }); @@ -94,6 +95,7 @@ export class RipgrepEngine { this.cancel(); process.removeListener('exit', this.killRgProcFn); done(null, { + type: 'success', limitHit: true, stats: null }); @@ -124,11 +126,13 @@ export class RipgrepEngine { process.removeListener('exit', this.killRgProcFn); if (stderr && !gotData && (displayMsg = rgErrorMsgForDisplay(stderr))) { done(new Error(displayMsg), { + type: 'success', limitHit: false, stats: null }); } else { done(null, { + type: 'success', limitHit: false, stats: null }); diff --git a/src/vs/workbench/services/search/node/search.ts b/src/vs/workbench/services/search/node/search.ts index a1061c52d22..7cf5124dd45 100644 --- a/src/vs/workbench/services/search/node/search.ts +++ b/src/vs/workbench/services/search/node/search.ts @@ -5,10 +5,11 @@ 'use strict'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import { IExpression } from 'vs/base/common/glob'; import { IProgress, ILineMatch, IPatternInfo, ISearchStats } from 'vs/platform/search/common/search'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { Event } from 'vs/base/common/event'; export interface IFolderSearch { folder: string; @@ -41,10 +42,10 @@ export interface ITelemetryEvent { } export interface IRawSearchService { - fileSearch(search: IRawSearch): PPromise; - textSearch(search: IRawSearch): PPromise; + fileSearch(search: IRawSearch): Event; + textSearch(search: IRawSearch): Event; clearCache(cacheKey: string): TPromise; - fetchTelemetry(): PPromise; + readonly onTelemetry: Event; } export interface IRawFileMatch { @@ -55,15 +56,37 @@ export interface IRawFileMatch { } export interface ISearchEngine { - search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void; + search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void) => void; cancel: () => void; } -export interface ISerializedSearchComplete { +export interface ISerializedSearchSuccess { + type: 'success'; limitHit: boolean; stats: ISearchStats; } +export interface ISerializedSearchError { + type: 'error'; + error: any; +} + +export type ISerializedSearchComplete = ISerializedSearchSuccess | ISerializedSearchError; + +export function isSerializedSearchComplete(arg: ISerializedSearchProgressItem | ISerializedSearchComplete): arg is ISerializedSearchComplete { + if ((arg as any).type === 'error') { + return true; + } else if ((arg as any).type === 'success') { + return true; + } else { + return false; + } +} + +export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg is ISerializedSearchSuccess { + return arg.type === 'success'; +} + export interface ISerializedFileMatch { path: string; lineMatches?: ILineMatch[]; diff --git a/src/vs/workbench/services/search/node/searchIpc.ts b/src/vs/workbench/services/search/node/searchIpc.ts index 4e9575e0d55..3450ab3e2fd 100644 --- a/src/vs/workbench/services/search/node/searchIpc.ts +++ b/src/vs/workbench/services/search/node/searchIpc.ts @@ -5,15 +5,16 @@ 'use strict'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IRawSearchService, IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent } from './search'; +import { Event } from 'vs/base/common/event'; export interface ISearchChannel extends IChannel { - call(command: 'fileSearch', search: IRawSearch): PPromise; - call(command: 'textSearch', search: IRawSearch): PPromise; + listen(event: 'telemetry'): Event; + listen(event: 'fileSearch', search: IRawSearch): Event; + listen(event: 'textSearch', search: IRawSearch): Event; call(command: 'clearCache', cacheKey: string): TPromise; - call(command: 'fetchTelemetry'): PPromise; call(command: string, arg: any): TPromise; } @@ -21,34 +22,38 @@ export class SearchChannel implements ISearchChannel { constructor(private service: IRawSearchService) { } - call(command: string, arg?: any): TPromise { - switch (command) { + listen(event: string, arg?: any): Event { + switch (event) { + case 'telemetry': return this.service.onTelemetry; case 'fileSearch': return this.service.fileSearch(arg); case 'textSearch': return this.service.textSearch(arg); - case 'clearCache': return this.service.clearCache(arg); - case 'fetchTelemetry': return this.service.fetchTelemetry(); } - return undefined; + throw new Error('Event not found'); + } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'clearCache': return this.service.clearCache(arg); + } + throw new Error('Call not found'); } } export class SearchChannelClient implements IRawSearchService { + get onTelemetry(): Event { return this.channel.listen('telemetry'); } + constructor(private channel: ISearchChannel) { } - fileSearch(search: IRawSearch): PPromise { - return this.channel.call('fileSearch', search); + fileSearch(search: IRawSearch): Event { + return this.channel.listen('fileSearch', search); } - textSearch(search: IRawSearch): PPromise { - return this.channel.call('textSearch', search); + textSearch(search: IRawSearch): Event { + return this.channel.listen('textSearch', search); } clearCache(cacheKey: string): TPromise { return this.channel.call('clearCache', cacheKey); } - - fetchTelemetry(): PPromise { - return this.channel.call('fetchTelemetry'); - } } \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index e7159cee743..bdc651e9950 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -15,7 +15,7 @@ import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService, ITelemetryEvent } from './search'; +import { IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService, ITelemetryEvent, isSerializedSearchComplete, isSerializedSearchSuccess, ISerializedSearchSuccess } from './search'; import { ISearchChannel, SearchChannelClient } from './searchIpc'; import { IEnvironmentService, IDebugParams } from 'vs/platform/environment/common/environment'; import { ResourceMap } from 'vs/base/common/map'; @@ -26,6 +26,7 @@ import { Schemas } from 'vs/base/common/network'; import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { Event } from 'vs/base/common/event'; export class SearchService implements ISearchService { public _serviceBrand: any; @@ -258,7 +259,12 @@ export class SearchService implements ISearchService { } public clearCache(cacheKey: string): TPromise { - return this.diskSearch.clearCache(cacheKey); + return TPromise.join([ + ...this.searchProviders, + this.fileSearchProvider, + this.diskSearch + ].map(provider => provider && provider.clearCache(cacheKey))) + .then(() => { }); } private forwardTelemetry() { @@ -315,14 +321,14 @@ export class DiskSearch implements ISearchResultProvider { const existingFolders = folderQueries.filter((q, index) => exists[index]); const rawSearch = this.rawSearchQuery(query, existingFolders); - let request: PPromise; + let event: Event; if (query.type === QueryType.File) { - request = this.raw.fileSearch(rawSearch); + event = this.raw.fileSearch(rawSearch); } else { - request = this.raw.textSearch(rawSearch); + event = this.raw.textSearch(rawSearch); } - return DiskSearch.collectResults(request); + return DiskSearch.collectResultsFromEvent(event); }); } @@ -367,7 +373,28 @@ export class DiskSearch implements ISearchResultProvider { return rawSearch; } - public static collectResults(request: PPromise): PPromise { + public static collectResultsFromEvent(event: Event): PPromise { + const promise = new PPromise((c, e, p) => { + setTimeout(() => { + const listener = event(ev => { + if (isSerializedSearchComplete(ev)) { + if (isSerializedSearchSuccess(ev)) { + c(ev); + } else { + e(ev.error); + } + listener.dispose(); + } else { + p(ev); + } + }); + }, 0); + }); + + return DiskSearch.collectResults(promise); + } + + public static collectResults(request: PPromise): PPromise { let result: IFileMatch[] = []; return new PPromise((c, e, p) => { request.done((complete) => { @@ -415,6 +442,8 @@ export class DiskSearch implements ISearchResultProvider { } public fetchTelemetry(): PPromise { - return this.raw.fetchTelemetry(); + return new PPromise((c, e, p) => { + this.raw.onTelemetry(p); + }); } } diff --git a/src/vs/workbench/services/search/node/textSearch.ts b/src/vs/workbench/services/search/node/textSearch.ts index c2002bb88ed..e2a14693c19 100644 --- a/src/vs/workbench/services/search/node/textSearch.ts +++ b/src/vs/workbench/services/search/node/textSearch.ts @@ -11,7 +11,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IProgress } from 'vs/platform/search/common/search'; import { FileWalker } from 'vs/workbench/services/search/node/fileSearch'; -import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine } from './search'; +import { ISerializedFileMatch, IRawSearch, ISearchEngine, ISerializedSearchSuccess } from './search'; import { ISearchWorker } from './worker/searchWorkerIpc'; import { ITextSearchWorkerProvider } from './textSearchWorkerProvider'; @@ -60,7 +60,7 @@ export class Engine implements ISearchEngine { }); } - search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void { this.workers = this.workerProvider.getWorkers(); this.initializeWorkers(); @@ -86,6 +86,7 @@ export class Engine implements ISearchEngine { if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) { this.isDone = true; done(this.walkerError, { + type: 'success', limitHit: this.limitReached, stats: this.walker.getStats() }); diff --git a/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts b/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts index 5260dc0b80d..70aade1c801 100644 --- a/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts +++ b/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts @@ -10,6 +10,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ISerializedFileMatch } from '../search'; import { IPatternInfo } from 'vs/platform/search/common/search'; import { SearchWorker } from './searchWorker'; +import { Event } from 'vs/base/common/event'; export interface ISearchWorkerSearchArgs { pattern: IPatternInfo; @@ -41,6 +42,10 @@ export class SearchWorkerChannel implements ISearchWorkerChannel { constructor(private worker: SearchWorker) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'initialize': return this.worker.initialize(); diff --git a/src/vs/workbench/services/search/test/node/searchService.test.ts b/src/vs/workbench/services/search/test/node/searchService.test.ts index 5d33088e881..9ceecbfa748 100644 --- a/src/vs/workbench/services/search/test/node/searchService.test.ts +++ b/src/vs/workbench/services/search/test/node/searchService.test.ts @@ -9,9 +9,11 @@ import * as assert from 'assert'; import * as path from 'path'; import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search'; -import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchComplete, IFolderSearch } from 'vs/workbench/services/search/node/search'; +import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, IFolderSearch, ISerializedSearchSuccess, ISerializedSearchProgressItem, ISerializedSearchComplete } from 'vs/workbench/services/search/node/search'; import { SearchService as RawSearchService } from 'vs/workbench/services/search/node/rawSearchService'; import { DiskSearch } from 'vs/workbench/services/search/node/searchService'; +import { Emitter, Event } from 'vs/base/common/event'; +import { TPromise } from 'vs/base/common/winjs.base'; const TEST_FOLDER_QUERIES = [ { folder: path.normalize('/some/where') } @@ -44,12 +46,13 @@ class TestSearchEngine implements ISearchEngine { TestSearchEngine.last = this; } - public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void { const self = this; (function next() { process.nextTick(() => { if (self.isCanceled) { done(null, { + type: 'success', limitHit: false, stats: stats }); @@ -58,6 +61,7 @@ class TestSearchEngine implements ISearchEngine { const result = self.result(); if (!result) { done(null, { + type: 'success', limitHit: false, stats: stats }); @@ -101,17 +105,17 @@ suite('SearchService', () => { const service = new RawSearchService(); let results = 0; - return service.doFileSearch(Engine, rawSearch) - .then(() => { - assert.strictEqual(results, 5); - }, null, value => { - if (!Array.isArray(value)) { - assert.deepStrictEqual(value, match); - results++; - } else { - assert.fail(JSON.stringify(value)); - } - }); + const cb: (p: ISerializedSearchProgressItem) => void = value => { + if (!Array.isArray(value)) { + assert.deepStrictEqual(value, match); + results++; + } else { + assert.fail(JSON.stringify(value)); + } + }; + + return service.doFileSearch(Engine, rawSearch, cb) + .then(() => assert.strictEqual(results, 5)); }); test('Batch results', function () { @@ -121,19 +125,20 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; - return service.doFileSearch(Engine, rawSearch, 10) - .then(() => { - assert.deepStrictEqual(results, [10, 10, 5]); - }, null, value => { - if (Array.isArray(value)) { - value.forEach(m => { - assert.deepStrictEqual(m, match); - }); - results.push(value.length); - } else { - assert.fail(JSON.stringify(value)); - } - }); + const cb: (p: ISerializedSearchProgressItem) => void = value => { + if (Array.isArray(value)) { + value.forEach(m => { + assert.deepStrictEqual(m, match); + }); + results.push(value.length); + } else { + assert.fail(JSON.stringify(value)); + } + }; + + return service.doFileSearch(Engine, rawSearch, cb, 10).then(() => { + assert.deepStrictEqual(results, [10, 10, 5]); + }); }); test('Collect batched results', function () { @@ -143,8 +148,24 @@ suite('SearchService', () => { const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch); const service = new RawSearchService(); + function fileSearch(config: IRawSearch, batchSize: number): Event { + let promise: TPromise; + + const emitter = new Emitter({ + onFirstListenerAdd: () => { + promise = service.doFileSearch(Engine, config, p => emitter.fire(p), batchSize) + .then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err })); + }, + onLastListenerRemove: () => { + promise.cancel(); + } + }); + + return emitter.event; + } + const progressResults = []; - return DiskSearch.collectResults(service.doFileSearch(Engine, rawSearch, 10)) + return DiskSearch.collectResultsFromEvent(fileSearch(rawSearch, 10)) .then(result => { assert.strictEqual(result.results.length, 25, 'Result'); assert.strictEqual(progressResults.length, 25, 'Progress'); @@ -167,7 +188,7 @@ suite('SearchService', () => { }, }; - return DiskSearch.collectResults(service.fileSearch(query)) + return DiskSearch.collectResultsFromEvent(service.fileSearch(query)) .then(result => { assert.strictEqual(result.results.length, 1, 'Result'); }); @@ -186,7 +207,7 @@ suite('SearchService', () => { }, }; - return DiskSearch.collectResults(service.fileSearch(query)) + return DiskSearch.collectResultsFromEvent(service.fileSearch(query)) .then(result => { assert.strictEqual(result.results.length, 0, 'Result'); assert.ok(result.limitHit); @@ -206,20 +227,22 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'bb', - sortByScore: true, - maxResults: 2 - }, 1).then(() => { - assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number'); - assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } + }; + + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'bb', + sortByScore: true, + maxResults: 2 + }, cb, 1).then(() => { + assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number'); + assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]); }); }); @@ -230,23 +253,24 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; + const cb = value => { + if (Array.isArray(value)) { + value.forEach(m => { + assert.deepStrictEqual(m, match); + }); + results.push(value.length); + } else { + assert.fail(JSON.stringify(value)); + } + }; return service.doFileSearch(Engine, { folderQueries: TEST_FOLDER_QUERIES, filePattern: 'a', sortByScore: true, maxResults: 23 - }, 10) + }, cb, 10) .then(() => { assert.deepStrictEqual(results, [10, 10, 3]); - }, null, value => { - if (Array.isArray(value)) { - value.forEach(m => { - assert.deepStrictEqual(m, match); - }); - results.push(value.length); - } else { - assert.fail(JSON.stringify(value)); - } }); }); @@ -263,37 +287,39 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'b', - sortByScore: true, - cacheKey: 'x' - }, -1).then(complete => { - assert.strictEqual(complete.stats.fromCache, false); - assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } + }; + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'b', + sortByScore: true, + cacheKey: 'x' + }, cb, -1).then(complete => { + assert.strictEqual(complete.stats.fromCache, false); + assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]); }).then(() => { const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'bc', - sortByScore: true, - cacheKey: 'x' - }, -1).then(complete => { - assert.ok(complete.stats.fromCache); - assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } - }); + }; + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'bc', + sortByScore: true, + cacheKey: 'x' + }, cb, -1).then(complete => { + assert.ok(complete.stats.fromCache); + assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]); + }, null); }).then(() => { return service.clearCache('x'); }).then(() => { @@ -304,20 +330,21 @@ suite('SearchService', () => { size: 3 }); const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'bc', - sortByScore: true, - cacheKey: 'x' - }, -1).then(complete => { - assert.strictEqual(complete.stats.fromCache, false); - assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } + }; + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'bc', + sortByScore: true, + cacheKey: 'x' + }, cb, -1).then(complete => { + assert.strictEqual(complete.stats.fromCache, false); + assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]); }); }); }); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index cf8b858ec15..f9e41489ea9 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -32,6 +32,7 @@ import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isLinux } from 'vs/base/common/platform'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; /** * The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk. @@ -42,7 +43,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100; private static saveErrorHandler: ISaveErrorHandler; + static setSaveErrorHandler(handler: ISaveErrorHandler): void { TextFileEditorModel.saveErrorHandler = handler; } + private static saveParticipant: ISaveParticipant; + static setSaveParticipant(handler: ISaveParticipant): void { TextFileEditorModel.saveParticipant = handler; } private readonly _onDidContentChange: Emitter = this._register(new Emitter()); get onDidContentChange(): Event { return this._onDidContentChange.event; } @@ -60,7 +64,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private blockModelContentChange: boolean; private autoSaveAfterMillies: number; private autoSaveAfterMilliesEnabled: boolean; - private autoSavePromise: TPromise; + private autoSaveDisposable: IDisposable; private contentChangeEventScheduler: RunOnceScheduler; private orphanedChangeEventScheduler: RunOnceScheduler; private saveSequentializer: SaveSequentializer; @@ -87,6 +91,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IHashService private hashService: IHashService ) { super(modelService, modeService); + this.resource = resource; this.preferredEncoding = preferredEncoding; this.inOrphanMode = false; @@ -177,63 +182,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void { - if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) { - this.autoSaveAfterMillies = config.autoSaveDelay; - this.autoSaveAfterMilliesEnabled = true; - } else { - this.autoSaveAfterMillies = void 0; - this.autoSaveAfterMilliesEnabled = false; - } + const autoSaveAfterMilliesEnabled = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0; + + this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled; + this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : void 0; } private onFilesAssociationChange(): void { - this.updateTextEditorModelMode(); - } - - private updateTextEditorModelMode(modeId?: string): void { if (!this.textEditorModel) { return; } const firstLineText = this.getFirstLineText(this.textEditorModel); - const mode = this.getOrCreateMode(this.modeService, modeId, firstLineText); + const mode = this.getOrCreateMode(this.modeService, void 0, firstLineText); this.modelService.setMode(this.textEditorModel, mode); } - /** - * The current version id of the model. - */ getVersionId(): number { return this.versionId; } - /** - * Set a save error handler to install code that executes when save errors occur. - */ - static setSaveErrorHandler(handler: ISaveErrorHandler): void { - TextFileEditorModel.saveErrorHandler = handler; - } - - /** - * Set a save participant handler to react on models getting saved. - */ - static setSaveParticipant(handler: ISaveParticipant): void { - TextFileEditorModel.saveParticipant = handler; - } - - /** - * Discards any local changes and replaces the model with the contents of the version on disk. - * - * @param if the parameter soft is true, will not attempt to load the contents from disk. - */ revert(soft?: boolean): TPromise { if (!this.isResolved()) { return TPromise.wrap(null); } // Cancel any running auto-save - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); // Unset flags const undo = this.setDirty(false); @@ -261,9 +237,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil load(options?: ILoadOptions): TPromise { diag('load() - enter', this.resource, new Date()); - // It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk - // if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk - // meanwhile, but this is a very low risk. + // It is very important to not reload the model when the model is dirty. + // We also only want to reload the model from the disk if no save is pending + // to avoid data loss. if (this.dirty || this.saveSequentializer.hasPendingSave()) { diag('load() - exit - without loading because model is dirty or being saved', this.resource, new Date()); @@ -272,14 +248,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Only for new models we support to load from backup if (!this.textEditorModel && !this.createTextEditorModelPromise) { - return this.loadWithBackup(options); + return this.loadFromBackup(options); } // Otherwise load from file resource return this.loadFromFile(options); } - private loadWithBackup(options?: ILoadOptions): TPromise { + private loadFromBackup(options?: ILoadOptions): TPromise { return this.backupFileService.loadBackupResource(this.resource).then(backup => { // Make sure meanwhile someone else did not suceed or start loading @@ -319,42 +295,55 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching } + // Ensure to track the versionId before doing a long running operation + // to make sure the model was not changed in the meantime which would + // indicate that the user or program has made edits. If we would ignore + // this, we could potentially loose the changes that were made because + // after resolving the content we update the model and reset the dirty + // flag. + const currentVersionId = this.versionId; + // Resolve Content return this.textFileService .resolveTextContent(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }) - .then(content => this.handleLoadSuccess(content), error => this.handleLoadError(error)); - } + .then(content => { - private handleLoadSuccess(content: IRawTextContent): TPromise { + // Clear orphaned state when loading was successful + this.setOrphaned(false); - // Clear orphaned state when load was successful - this.setOrphaned(false); + // Guard against the model having changed in the meantime + if (currentVersionId === this.versionId) { + return this.loadWithContent(content); + } - return this.loadWithContent(content); - } + return this; + }, error => { + const result = error.fileOperationResult; - private handleLoadError(error: FileOperationError): TPromise { - const result = error.fileOperationResult; + // Apply orphaned state based on error code + this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); - // Apply orphaned state based on error code - this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); + // NotModified status is expected and can be handled gracefully + if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { - // NotModified status is expected and can be handled gracefully - if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { - this.setDirty(false); // Ensure we are not tracking a stale state + // Guard against the model having changed in the meantime + if (currentVersionId === this.versionId) { + this.setDirty(false); // Ensure we are not tracking a stale state + } - return TPromise.as(this); - } + return TPromise.as(this); + } - // Ignore when a model has been resolved once and the file was deleted meanwhile. Since - // we already have the model loaded, we can return to this state and update the orphaned - // flag to indicate that this model has no version on disk anymore. - if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) { - return TPromise.as(this); - } + // Ignore when a model has been resolved once and the file was deleted meanwhile. Since + // we already have the model loaded, we can return to this state and update the orphaned + // flag to indicate that this model has no version on disk anymore. + if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) { + return TPromise.as(this); + } - // Otherwise bubble up the error - return TPromise.wrapError(error); + // Otherwise bubble up the error + return TPromise.wrapError(error); + }); } private loadWithContent(content: IRawTextContent, backup?: URI): TPromise { @@ -385,7 +374,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil diag('load() - resolved content', this.resource, new Date()); // Update our resolved disk stat model - const resolvedStat: IFileStat = { + this.updateLastResolvedDiskStat({ resource: this.resource, name: content.name, mtime: content.mtime, @@ -394,8 +383,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil isSymbolicLink: false, children: void 0, isReadonly: content.isReadonly - }; - this.updateLastResolvedDiskStat(resolvedStat); + } as IFileStat); // Keep the original encoding to not loose it when saving const oldEncoding = this.contentEncoding; @@ -565,34 +553,31 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private doAutoSave(versionId: number): TPromise { + private doAutoSave(versionId: number): void { diag(`doAutoSave() - enter for versionId ${versionId}`, this.resource, new Date()); // Cancel any currently running auto saves to make this the one that succeeds - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); - // Create new save promise and keep it - this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => { + // Create new save timer and store it for disposal as needed + const handle = setTimeout(() => { // Only trigger save if the version id has not changed meanwhile if (versionId === this.versionId) { this.doSave(versionId, { reason: SaveReason.AUTO }).done(null, onUnexpectedError); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change } - }); + }, this.autoSaveAfterMillies); - return this.autoSavePromise; + this.autoSaveDisposable = toDisposable(() => clearTimeout(handle)); } - private cancelAutoSavePromise(): void { - if (this.autoSavePromise) { - this.autoSavePromise.cancel(); - this.autoSavePromise = void 0; + private cancelPendingAutoSave(): void { + if (this.autoSaveDisposable) { + this.autoSaveDisposable.dispose(); + this.autoSaveDisposable = void 0; } } - /** - * Saves the current versionId of this editor model if it is dirty. - */ save(options: ISaveOptions = Object.create(null)): TPromise { if (!this.isResolved()) { return TPromise.wrap(null); @@ -601,7 +586,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil diag('save() - enter', this.resource, new Date()); // Cancel any currently running auto saves to make this the one that succeeds - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); return this.doSave(this.versionId, options); } @@ -861,30 +846,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil TextFileEditorModel.saveErrorHandler.onSaveError(error, this); } - /** - * Returns true if the content of this model has changes that are not yet saved back to the disk. - */ isDirty(): boolean { return this.dirty; } - /** - * Returns the time in millies when this working copy was attempted to be saved. - */ getLastSaveAttemptTime(): number { return this.lastSaveAttemptTime; } - /** - * Returns the time in millies when this working copy was last modified by the user or some other program. - */ getETag(): string { return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag : null; } - /** - * Answers if this model is in a specific state. - */ hasState(state: ModelState): boolean { switch (state) { case ModelState.CONFLICT: @@ -974,23 +947,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly; } - /** - * Returns true if the dispose() method of this model has been called. - */ isDisposed(): boolean { return this.disposed; } - /** - * Returns the full resource URI of the file this text file editor model is about. - */ getResource(): URI { return this.resource; } - /** - * Stat accessor only used by tests. - */ getStat(): IFileStat { return this.lastResolvedDiskStat; } @@ -1003,7 +967,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.createTextEditorModelPromise = null; - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); super.dispose(); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index c37e191b716..32e0164ecae 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -13,6 +13,7 @@ import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeE import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { @@ -142,7 +143,17 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE let model = this.get(resource); if (model) { if (options && options.reload) { - modelPromise = model.load(modelLoadOptions); + + // async reload: trigger a reload but return immediately + if (options.reload.async) { + modelPromise = TPromise.as(model); + model.load(options).then(null, onUnexpectedError); + } + + // sync reload: do not return until model reloaded + else { + modelPromise = model.load(modelLoadOptions); + } } else { modelPromise = TPromise.as(model); } diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 6a6b4648038..f77b7d2ecc1 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -692,6 +692,23 @@ export abstract class TextFileService extends Disposable implements ITextFileSer }); } + create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise { + const existingModel = this.models.get(resource); + + return this.fileService.createFile(resource, contents, options).then(() => { + + // If we had an existing model for the given resource, load + // it again to make sure it is up to date with the contents + // we just wrote into the underlying resource by calling + // revert() + if (existingModel && !existingModel.isDisposed()) { + return existingModel.revert(); + } + + return void 0; + }); + } + delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise { const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */)); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 5329c91f2e2..737fbaed9c4 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -147,9 +147,16 @@ export interface IModelLoadOrCreateOptions { encoding?: string; /** - * Wether to reload the model if it already exists. + * If the model was already loaded before, allows to trigger + * a reload of it to fetch the latest contents: + * - async: loadOrCreate() will return immediately and trigger + * a reload that will run in the background. + * - sync: loadOrCreate() will only return resolved when the + * model has finished reloading. */ - reload?: boolean; + reload?: { + async: boolean + }; /** * Allow to load a model even if we think it is a binary file. @@ -321,6 +328,12 @@ export interface ITextFileService extends IDisposable { */ revertAll(resources?: URI[], options?: IRevertOptions): TPromise; + /** + * Create a file. If the file exists it will be overwritten with the contents if + * the options enable to overwrite. + */ + create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise; + /** * Delete a file. If the file is dirty, it will get reverted and then deleted from disk. */ diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index b07a9c25b47..fee25713dd3 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -105,7 +105,7 @@ suite('Files - TextFileEditorModelManager', () => { const resource = URI.file('/test.html'); const encoding = 'utf8'; - return manager.loadOrCreate(resource, { encoding, reload: true }).then(model => { + return manager.loadOrCreate(resource, { encoding }).then(model => { assert.ok(model); assert.equal(model.getEncoding(), encoding); assert.equal(manager.get(resource), model); diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 598ed4084f2..a8b690fc006 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -188,9 +188,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // Stop the extension host first to give extensions most time to shutdown this.extensionService.stopExtensionHost(); + let extensionHostStarted: boolean = false; const startExtensionHost = () => { this.extensionService.startExtensionHost(); + extensionHostStarted = true; }; return mainSidePromise().then(result => { @@ -206,14 +208,15 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // Reinitialize configuration service const workspaceImpl = this.contextService as WorkspaceService; - return workspaceImpl.initialize(result.workspace); + return workspaceImpl.initialize(result.workspace, startExtensionHost); }); } return TPromise.as(void 0); - }).then(startExtensionHost, error => { - startExtensionHost(); // in any case start the extension host again! - + }).then(null, error => { + if (!extensionHostStarted) { + startExtensionHost(); // start the extension host if not started + } return TPromise.wrapError(error); }); } 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 7cbde0bb8c9..fcb377a0c43 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -71,7 +71,7 @@ class MyInput extends EditorInput { return ''; } - resolve(refresh?: boolean): any { + resolve(): any { return null; } } @@ -81,7 +81,7 @@ class MyOtherInput extends EditorInput { return ''; } - resolve(refresh?: boolean): any { + resolve(): any { return null; } } diff --git a/src/vs/workbench/test/browser/parts/editor/editorBreadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorBreadcrumbModel.test.ts new file mode 100644 index 00000000000..c0942cd5dc4 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/editorBreadcrumbModel.test.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import URI from 'vs/base/common/uri'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/editorBreadcrumbsModel'; +import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; + + +suite('Breadcrumb Model', function () { + + const workspaceService = new TestContextService(new Workspace('ffff', 'Test', [new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws', index: 0 })])); + + test('only uri, inside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, workspaceService); + let elements = model.getElements(); + + assert.equal(elements.length, 3); + let [one, two, three] = elements as FileElement[]; + assert.equal(one.isFile, false); + assert.equal(two.isFile, false); + assert.equal(three.isFile, true); + assert.equal(one.uri.toString(), 'foo:/bar/baz/ws/some'); + assert.equal(two.uri.toString(), 'foo:/bar/baz/ws/some/path'); + assert.equal(three.uri.toString(), 'foo:/bar/baz/ws/some/path/file.ts'); + }); + + test('only uri, outside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, workspaceService); + let elements = model.getElements(); + + assert.equal(elements.length, 2); + let [one, two] = elements as FileElement[]; + assert.equal(one.isFile, false); + assert.equal(two.isFile, true); + assert.equal(one.uri.toString(), 'foo:/outside'); + assert.equal(two.uri.toString(), 'foo:/outside/file.ts'); + }); +}); diff --git a/src/vs/workbench/test/common/editor/editor.test.ts b/src/vs/workbench/test/common/editor/editor.test.ts index 4e169cf7e1d..3fefb60de2d 100644 --- a/src/vs/workbench/test/common/editor/editor.test.ts +++ b/src/vs/workbench/test/common/editor/editor.test.ts @@ -35,7 +35,7 @@ class FileEditorInput extends EditorInput { return this.resource; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } } diff --git a/src/vs/workbench/test/common/editor/editorDiffModel.test.ts b/src/vs/workbench/test/common/editor/editorDiffModel.test.ts index b4ce15928a4..b8a7e0f3411 100644 --- a/src/vs/workbench/test/common/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorDiffModel.test.ts @@ -55,7 +55,7 @@ suite('Workbench editor model', () => { let otherInput = instantiationService.createInstance(ResourceEditorInput, 'name2', 'description', URI.from({ scheme: 'test', authority: null, path: 'thePath' })); let diffInput = new DiffEditorInput('name', 'description', input, otherInput); - return diffInput.resolve(true).then((model: any) => { + return diffInput.resolve().then((model: any) => { assert(model); assert(model instanceof TextDiffEditorModel); @@ -63,7 +63,7 @@ suite('Workbench editor model', () => { assert(diffEditorModel.original); assert(diffEditorModel.modified); - return diffInput.resolve(true).then((model: any) => { + return diffInput.resolve().then((model: any) => { assert(model.isResolved()); assert(diffEditorModel !== model.textDiffEditorModel); diff --git a/src/vs/workbench/test/common/editor/editorInput.test.ts b/src/vs/workbench/test/common/editor/editorInput.test.ts index b162d802d25..94eea4a8805 100644 --- a/src/vs/workbench/test/common/editor/editorInput.test.ts +++ b/src/vs/workbench/test/common/editor/editorInput.test.ts @@ -11,7 +11,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; class MyEditorInput extends EditorInput { getTypeId(): string { return ''; } - resolve(refresh?: boolean): any { return null; } + resolve(): any { return null; } } suite('Workbench editor input', () => { diff --git a/src/vs/workbench/test/common/editor/editorOptions.test.ts b/src/vs/workbench/test/common/editor/editorOptions.test.ts index f5dd20c857f..345b58f4833 100644 --- a/src/vs/workbench/test/common/editor/editorOptions.test.ts +++ b/src/vs/workbench/test/common/editor/editorOptions.test.ts @@ -16,12 +16,12 @@ suite('Workbench editor options', () => { assert(!options.preserveFocus); options.preserveFocus = true; assert(options.preserveFocus); - assert(!options.forceOpen); - options.forceOpen = true; - assert(options.forceOpen); + assert(!options.forceReload); + options.forceReload = true; + assert(options.forceReload); options = new EditorOptions(); - options.forceOpen = true; + options.forceReload = true; }); test('TextEditorOptions', function () { @@ -35,7 +35,7 @@ suite('Workbench editor options', () => { otherOptions.selection(1, 1, 2, 2); options = new TextEditorOptions(); - options.forceOpen = true; + options.forceReload = true; options.selection(1, 1, 2, 2); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index ba37b81ff7d..80a57fac79c 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -450,6 +450,38 @@ suite('ExtHostLanguageFeatureCommands', function () { }); + test('"vscode.executeCompletionItemProvider" doesnot return a preselect field #53749', async function () { + disposables.push(extHost.registerCompletionItemProvider(defaultSelector, { + provideCompletionItems(): any { + let a = new types.CompletionItem('item1'); + a.preselect = true; + let b = new types.CompletionItem('item2'); + let c = new types.CompletionItem('item3'); + c.preselect = true; + let d = new types.CompletionItem('item4'); + return new types.CompletionList([a, b, c, d], false); + } + }, [])); + + await rpcProtocol.sync(); + + let list = await commands.executeCommand( + 'vscode.executeCompletionItemProvider', + model.uri, + new types.Position(0, 4), + undefined + ); + + assert.ok(list instanceof types.CompletionList); + assert.equal(list.items.length, 4); + + let [a, b, c, d] = list.items; + assert.equal(a.preselect, true); + assert.equal(b.preselect, undefined); + assert.equal(c.preselect, true); + assert.equal(d.preselect, undefined); + }); + // --- quickfix test('QuickFix, back and forth', function () { diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index f5479d67f9e..b9995cf46f8 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -46,6 +46,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { NullLogService } from 'vs/platform/log/common/log'; import { ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; import { getColors } from 'vs/editor/contrib/colorPicker/color'; +import { CancellationToken } from 'vs/base/common/cancellation'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = EditorModel.createFromString( @@ -203,7 +204,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); }); }); @@ -224,7 +225,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let data = value[0]; @@ -248,7 +249,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let data = value[0]; @@ -490,7 +491,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); @@ -514,7 +515,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); @@ -538,7 +539,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 3 }); @@ -563,7 +564,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); }); @@ -587,7 +588,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideReferences(model, new EditorPosition(1, 2)).then(value => { + return provideReferences(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 2); let [first, second] = value; @@ -607,7 +608,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideReferences(model, new EditorPosition(1, 2)).then(value => { + return provideReferences(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [item] = value; @@ -633,7 +634,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideReferences(model, new EditorPosition(1, 2)).then(value => { + return provideReferences(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); @@ -871,7 +872,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideSignatureHelp(model, new EditorPosition(1, 1)).then(value => { + return provideSignatureHelp(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.ok(value); }); }); @@ -886,7 +887,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideSignatureHelp(model, new EditorPosition(1, 1)).then(value => { + return provideSignatureHelp(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value, undefined); }); }); @@ -1151,7 +1152,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getLinks(model).then(value => { + return getLinks(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; @@ -1176,7 +1177,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getLinks(model).then(value => { + return getLinks(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; @@ -1198,7 +1199,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getColors(model).then(value => { + return getColors(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index 7cb103589ca..49d33da63b2 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -5,7 +5,6 @@ 'use strict'; import * as assert from 'assert'; -import * as path from 'path'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; @@ -36,12 +35,12 @@ class MockMainThreadSearch implements MainThreadSearchShape { $unregisterProvider(handle: number): void { } - $handleFindMatch(handle: number, session: number, data: UriComponents | IRawFileMatch2[]): void { - if (Array.isArray(data)) { - this.results.push(...data); - } else { - this.results.push(data); - } + $handleFileMatch(handle: number, session: number, data: UriComponents[]): void { + this.results.push(...data); + } + + $handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void { + this.results.push(...data); } $handleTelemetry(eventName: string, data: any): void { @@ -155,7 +154,7 @@ suite('ExtHostSearch', () => { test('no results', async () => { await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { return TPromise.wrap(null); } }); @@ -173,8 +172,8 @@ suite('ExtHostSearch', () => { ]; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); } }); @@ -188,11 +187,11 @@ suite('ExtHostSearch', () => { test('Search canceled', async () => { let cancelRequested = false; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { return new TPromise((resolve, reject) => { token.onCancellationRequested(() => { cancelRequested = true; - progress.report('file1.ts'); + progress.report(joinPath(options.folder, 'file1.ts')); resolve(null); // or reject or nothing? }); @@ -213,8 +212,11 @@ suite('ExtHostSearch', () => { ]; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(r)); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults + .map(relativePath => joinPath(options.folder, relativePath)) + .forEach(r => progress.report(r)); + throw new Error('I broke'); } }); @@ -229,7 +231,7 @@ suite('ExtHostSearch', () => { test('provider returns null', async () => { await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { return null; } }); @@ -244,7 +246,7 @@ suite('ExtHostSearch', () => { test('all provider calls get global include/excludes', async () => { await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { assert(options.excludes.length === 2 && options.includes.length === 2, 'Missing global include/excludes'); return TPromise.wrap(null); } @@ -273,7 +275,7 @@ suite('ExtHostSearch', () => { test('global/local include/excludes combined', async () => { await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { if (options.folder.toString() === rootFolderA.toString()) { assert.deepEqual(options.includes.sort(), ['*.ts', 'foo']); assert.deepEqual(options.excludes.sort(), ['*.js', 'bar']); @@ -315,7 +317,7 @@ suite('ExtHostSearch', () => { test('include/excludes resolved correctly', async () => { await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { assert.deepEqual(options.includes.sort(), ['*.jsx', '*.ts']); assert.deepEqual(options.excludes.sort(), []); @@ -358,8 +360,10 @@ suite('ExtHostSearch', () => { ]; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(r)); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults + .map(relativePath => joinPath(options.folder, relativePath)) + .forEach(r => progress.report(r)); return TPromise.wrap(null); } }); @@ -389,20 +393,20 @@ suite('ExtHostSearch', () => { test('multiroot sibling exclude clause', async () => { await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - let reportedResults; + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + let reportedResults: URI[]; if (options.folder.fsPath === rootFolderA.fsPath) { reportedResults = [ 'folder/fileA.scss', 'folder/fileA.css', 'folder/file2.css' - ]; + ].map(relativePath => joinPath(rootFolderA, relativePath)); } else { reportedResults = [ 'fileB.ts', 'fileB.js', 'file3.js' - ]; + ].map(relativePath => joinPath(rootFolderB, relativePath)); } reportedResults.forEach(r => progress.report(r)); @@ -460,8 +464,9 @@ suite('ExtHostSearch', () => { let wasCanceled = false; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults + .forEach(r => progress.report(r)); token.onCancellationRequested(() => wasCanceled = true); return TPromise.wrap(null); @@ -497,8 +502,8 @@ suite('ExtHostSearch', () => { let wasCanceled = false; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(r)); token.onCancellationRequested(() => wasCanceled = true); return TPromise.wrap(null); @@ -533,8 +538,8 @@ suite('ExtHostSearch', () => { let wasCanceled = false; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(r)); token.onCancellationRequested(() => wasCanceled = true); return TPromise.wrap(null); @@ -564,7 +569,7 @@ suite('ExtHostSearch', () => { test('multiroot max results', async () => { let cancels = 0; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => cancels++); // Provice results async so it has a chance to invoke every provider @@ -574,9 +579,8 @@ suite('ExtHostSearch', () => { 'file1.ts', 'file2.ts', 'file3.ts', - ].forEach(f => { - progress.report(f); - }); + ].map(relativePath => joinPath(options.folder, relativePath)) + .forEach(r => progress.report(r)); }); } }); @@ -610,8 +614,8 @@ suite('ExtHostSearch', () => { ]; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); } }); @@ -642,8 +646,8 @@ suite('ExtHostSearch', () => { ]; await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); } }, fancyScheme); @@ -662,27 +666,33 @@ suite('ExtHostSearch', () => { compareURIs(results, reportedResults); }); - // Mock fs? - // test('Returns result for absolute path', async () => { - // const queriedFile = makeFileResult(rootFolderA, 'file2.ts'); - // const reportedResults = [ - // makeFileResult(rootFolderA, 'file1.ts'), - // queriedFile, - // makeFileResult(rootFolderA, 'file3.ts'), - // ]; + test('uses different cache keys for different folders', async () => { + const cacheKeys: string[] = []; + await registerTestSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + cacheKeys.push(query.cacheKey); + return TPromise.wrap(null); + } + }); - // await registerTestSearchProvider({ - // provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - // reportedResults.forEach(r => progress.report(r)); - // return TPromise.wrap(null); - // } - // }); + const query: ISearchQuery = { + type: QueryType.File, + filePattern: '', + cacheKey: 'cacheKey', + folderQueries: [ + { + folder: rootFolderA + }, + { + folder: rootFolderB + } + ] + }; - // const queriedFilePath = queriedFile.fsPath; - // const { results } = await runFileSearch(getSimpleQuery(queriedFilePath)); - // assert.equal(results.length, 1); - // compareURIs(results, [queriedFile]); - // }); + await runFileSearch(query); + assert.equal(cacheKeys.length, 2); + assert.notEqual(cacheKeys[0], cacheKeys[1]); + }); }); suite('Text:', () => { @@ -694,11 +704,11 @@ suite('ExtHostSearch', () => { }; } - function makeTextResult(relativePath: string): vscode.TextSearchResult { + function makeTextResult(baseFolder: URI, relativePath: string): vscode.TextSearchResult { return { preview: makePreview('foo'), range: new Range(0, 0, 0, 3), - path: relativePath + uri: joinPath(baseFolder, relativePath) }; } @@ -718,17 +728,16 @@ suite('ExtHostSearch', () => { }; } - function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[], folder = rootFolderA) { + function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) { const actualTextSearchResults: vscode.TextSearchResult[] = []; for (let fileMatch of actual) { // Make relative - const relativePath = fileMatch.resource.toString().substr(folder.toString().length + 1); for (let lineMatch of fileMatch.lineMatches) { for (let [offset, length] of lineMatch.offsetAndLengths) { actualTextSearchResults.push({ preview: { text: lineMatch.preview, match: null }, range: new Range(lineMatch.lineNumber, offset, lineMatch.lineNumber, length + offset), - path: relativePath + uri: fileMatch.resource }); } } @@ -741,7 +750,7 @@ suite('ExtHostSearch', () => { .map(r => ({ ...r, ...{ - uri: r.path.toString(), + uri: r.uri.toString(), range: rangeToString(r.range), preview: { text: r.preview.text, @@ -769,8 +778,8 @@ suite('ExtHostSearch', () => { test('basic results', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts') ]; await registerTestSearchProvider({ @@ -920,8 +929,8 @@ suite('ExtHostSearch', () => { }; const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.js'), - makeTextResult('file1.ts') + makeTextResult(rootFolderA, 'file1.js'), + makeTextResult(rootFolderA, 'file1.ts') ]; await registerTestSearchProvider({ @@ -973,15 +982,15 @@ suite('ExtHostSearch', () => { let reportedResults; if (options.folder.fsPath === rootFolderA.fsPath) { reportedResults = [ - makeTextResult('folder/fileA.scss'), - makeTextResult('folder/fileA.css'), - makeTextResult('folder/file2.css') + makeTextResult(rootFolderA, 'folder/fileA.scss'), + makeTextResult(rootFolderA, 'folder/fileA.css'), + makeTextResult(rootFolderA, 'folder/file2.css') ]; } else { reportedResults = [ - makeTextResult('fileB.ts'), - makeTextResult('fileB.js'), - makeTextResult('file3.js') + makeTextResult(rootFolderB, 'fileB.ts'), + makeTextResult(rootFolderB, 'fileB.js'), + makeTextResult(rootFolderB, 'file3.js') ]; } @@ -1019,17 +1028,17 @@ suite('ExtHostSearch', () => { const { results } = await runTextSearch(getPattern('foo'), query); assertResults(results, [ - makeTextResult('folder/fileA.scss'), - makeTextResult('folder/file2.css'), - makeTextResult('fileB.ts'), - makeTextResult('fileB.js'), - makeTextResult('file3.js')]); + makeTextResult(rootFolderA, 'folder/fileA.scss'), + makeTextResult(rootFolderA, 'folder/file2.css'), + makeTextResult(rootFolderB, 'fileB.ts'), + makeTextResult(rootFolderB, 'fileB.js'), + makeTextResult(rootFolderB, 'file3.js')]); }); test('include pattern applied', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.js'), - makeTextResult('file1.ts') + makeTextResult(rootFolderA, 'file1.js'), + makeTextResult(rootFolderA, 'file1.ts') ]; await registerTestSearchProvider({ @@ -1057,8 +1066,8 @@ suite('ExtHostSearch', () => { test('max results = 1', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts') ]; let wasCanceled = false; @@ -1088,9 +1097,9 @@ suite('ExtHostSearch', () => { test('max results = 2', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts'), - makeTextResult('file3.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts'), + makeTextResult(rootFolderA, 'file3.ts') ]; let wasCanceled = false; @@ -1120,8 +1129,8 @@ suite('ExtHostSearch', () => { test('provider returns maxResults exactly', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts') ]; let wasCanceled = false; @@ -1160,7 +1169,7 @@ suite('ExtHostSearch', () => { 'file1.ts', 'file2.ts', 'file3.ts', - ].forEach(f => progress.report(makeTextResult(f))); + ].forEach(f => progress.report(makeTextResult(options.folder, f))); }); } }); @@ -1183,9 +1192,9 @@ suite('ExtHostSearch', () => { test('works with non-file schemes', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts'), - makeTextResult('file3.ts') + makeTextResult(fancySchemeFolderA, 'file1.ts'), + makeTextResult(fancySchemeFolderA, 'file2.ts'), + makeTextResult(fancySchemeFolderA, 'file3.ts') ]; await registerTestSearchProvider({ @@ -1204,7 +1213,7 @@ suite('ExtHostSearch', () => { }; const { results } = await runTextSearch(getPattern('foo'), query); - assertResults(results, providedResults, fancySchemeFolderA); + assertResults(results, providedResults); }); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index 5b03bd47a60..4568ce2703c 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -528,9 +528,9 @@ suite('ExtHostWorkspace', function () { assert.throws(() => { (e).added = []; }); - assert.throws(() => { - (e.added)[0] = null; - }); + // assert.throws(() => { + // (e.added)[0] = null; + // }); } catch (error) { finish(error); } diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts index 41120cfed58..0589271ab33 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -23,7 +23,6 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IFileStat } from 'vs/platform/files/common/files'; import { ResourceTextEdit } from 'vs/editor/common/modes'; import { BulkEditService } from 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -48,16 +47,14 @@ suite('MainThreadEditors', () => { createdResources.clear(); deletedResources.clear(); - const fileService = new class extends TestFileService { - createFile(uri: URI): TPromise { - createdResources.add(uri); - return TPromise.wrap(createMockFileStat(uri)); - } - }; - + const fileService = new TestFileService(); const textFileService = new class extends mock() { isDirty() { return false; } + create(uri: URI, contents?: string, options?: any) { + createdResources.add(uri); + return TPromise.as(void 0); + } delete(resource: URI) { deletedResources.add(resource); return TPromise.as(void 0); @@ -75,7 +72,7 @@ suite('MainThreadEditors', () => { const workbenchEditorService = new TestEditorService(); const editorGroupService = new TestEditorGroupsService(); - const bulkEditService = new BulkEditService(new NullLogService(), modelService, new TestEditorService(), null, fileService, textFileService, TestEnvironmentService, new TestContextService()); + const bulkEditService = new BulkEditService(new NullLogService(), modelService, new TestEditorService(), null, new TestFileService(), textFileService, TestEnvironmentService, new TestContextService()); const rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(ExtHostContext.ExtHostDocuments, new class extends mock() { @@ -132,7 +129,7 @@ suite('MainThreadEditors', () => { }); }); - test(`applyWorkspaceEdit with only resource edit`, () => { + test(`pasero applyWorkspaceEdit with only resource edit`, () => { return editors.$tryApplyWorkspaceEdit({ edits: [ { oldUri: resource, newUri: resource, options: undefined }, @@ -147,14 +144,3 @@ suite('MainThreadEditors', () => { }); }); }); - - -function createMockFileStat(target: URI): IFileStat { - return { - etag: '', - isDirectory: false, - name: target.path, - mtime: 0, - resource: target - }; -} diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 954b62cfee8..35a157ea0c1 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -66,8 +66,8 @@ import { IExtensionService, ProfileSession, IExtensionsStatus, ExtensionPointCon import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout, IEditorBreadcrumbs } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -286,7 +286,7 @@ export function workbenchInstantiationService(): IInstantiationService { export class TestDecorationsService implements IDecorationsService { _serviceBrand: any; onDidChangeDecorations: Event = Event.None; - registerDecorationsProvider(provider: IDecorationsProvider): IDisposable { return toDisposable(); } + registerDecorationsProvider(provider: IDecorationsProvider): IDisposable { return Disposable.None; } getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration { return void 0; } } @@ -583,6 +583,7 @@ export class TestEditorGroup implements IEditorGroupView { constructor(public id: number) { } group: EditorGroup = void 0; + breadcrumbs: IEditorBreadcrumbs; activeControl: IEditor; activeEditor: IEditorInput; previewEditor: IEditorInput; diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index 69bb655de6e..71023e7e91f 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -5,13 +5,22 @@ const path = require('path'); const Mocha = require('mocha'); +const minimist = require('minimist'); const suite = 'Smoke Tests'; +const [, , ...args] = process.argv; +const opts = minimist(args, { + string: [ + 'f' + ] +}); + const options = { useColors: true, timeout: 60000, - slow: 30000 + slow: 30000, + grep: opts['f'] }; if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { diff --git a/yarn.lock b/yarn.lock index b1404be9a6a..f49e290a360 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6256,9 +6256,9 @@ vscode-textmate@^4.0.1: dependencies: oniguruma "^7.0.0" -vscode-xterm@3.5.0-beta17: - version "3.5.0-beta17" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.5.0-beta17.tgz#9a7dd13cdd9a45257aacb815501f71ed788c2e48" +vscode-xterm@3.6.0-beta2: + version "3.6.0-beta2" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.6.0-beta2.tgz#db2991cc2e73c7f5c30bd85b1096cb5d38112589" vso-node-api@^6.1.2-preview: version "6.1.2-preview"