diff --git a/.github/calendar.yml b/.github/calendar.yml index a08deeaee6c..6d3e0f593d7 100644 --- a/.github/calendar.yml +++ b/.github/calendar.yml @@ -21,5 +21,9 @@ '2018-05-15 12:00, US/Pacific': 'development', '2018-05-28 18:00, US/Pacific': 'endgame', # 'release' not needed anymore, return to 'development' after releasing. - '2018-06-06 12:00, US/Pacific': 'development', + '2018-06-06 12:00, US/Pacific': 'development', # 1.24.0 released + '2018-06-25 18:00, US/Pacific': 'endgame', + '2018-07-05 12:00, US/Pacific': 'development', # 1.25.0 released + '2018-07-30 18:00, US/Pacific': 'endgame', + # '2018-08-08 12:00, US/Pacific': 'development', # 1.26.0 released } diff --git a/.github/commands.yml b/.github/commands.yml index ba8c8d7f3ce..56edba1d4da 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -29,7 +29,7 @@ type: 'label', name: '*out-of-scope', action: 'close', - comment: "This issue is being closed to keep the number of issues in our inbox on a manageable level, we are closing issues that have been on the backlog for a long time but have not gained traction: We look at the number of votes the issue has received and the number of duplicate issues filed. If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" + comment: "This issue is being closed to keep the number of issues in our inbox on a manageable level, we are closing issues that are not going to be addressed in the foreseeable future: We look at the number of votes the issue has received and the number of duplicate issues filed. If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" }, { type: 'label', @@ -62,5 +62,12 @@ action: 'comment', comment: "Potential duplicates:\n${potentialDuplicates}" }, + { + type: 'comment', + name: 'needsMoreInfo', + action: 'updateLabels', + addLabel: 'needs more info', + comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!" + }, ] } diff --git a/.gitignore b/.gitignore index 0b257508fe9..08adb4af663 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ node_modules/ out/ out-build/ out-editor/ +out-editor-src/ +out-editor-build/ out-editor-esm/ out-editor-min/ out-monaco-editor-core/ diff --git a/.vscode/launch.json b/.vscode/launch.json index d8951d91273..2a12dea7bde 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -236,6 +236,15 @@ "VSCODE_DEV": "1", "VSCODE_CLI": "1" } + }, + { + "name": "Launch Built-in Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}/extensions/debug-auto-launch" + ] } ], "compounds": [ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09ee1ca9270..0d3f7dc4bf3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ Please include the following with each issue: * What you expected to see, versus what you actually saw -* Images, animations, or a link to a video showing the issue occuring +* Images, animations, or a link to a video showing the issue occurring * A code snippet that demonstrates the issue or a link to a code repository the developers can easily pull down to recreate the issue locally diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 2d53c46e935..499d1df761d 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.5", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { "name": "ms-vscode.node-debug2", - "version": "1.26.0", + "version": "1.26.5", "repo": "https://github.com/Microsoft/vscode-node-debug2" } ] diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 3fc14da5045..562ee1dd754 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -12,10 +12,12 @@ const File = require('vinyl'); const i18n = require('./lib/i18n'); const standalone = require('./lib/standalone'); const cp = require('child_process'); +const compilation = require('./lib/compilation'); +const monacoapi = require('./monaco/api'); +const fs = require('fs'); 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 + ')'; @@ -59,29 +61,56 @@ var BUNDLED_FILE_HEADER = [ '' ].join('\n'); -function editorLoaderConfig() { - var result = common.loaderConfig(); - - // never ship octicons in editor - result.paths['vs/base/browser/ui/octiconLabel/octiconLabel'] = 'out-build/vs/base/browser/ui/octiconLabel/octiconLabel.mock'; - - // force css inlining to use base64 -- see https://github.com/Microsoft/monaco-editor/issues/148 - result['vs/css'] = { - inlineResources: 'base64', - inlineResourcesLimit: 3000 // see https://github.com/Microsoft/monaco-editor/issues/336 - }; - - return result; -} - const languages = i18n.defaultLanguages.concat([]); // i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages : []); +gulp.task('clean-editor-src', util.rimraf('out-editor-src')); +gulp.task('extract-editor-src', ['clean-editor-src'], function () { + console.log(`If the build fails, consider tweaking shakeLevel below to a lower value.`); + const apiusages = monacoapi.execute().usageContent; + const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString(); + standalone.extractEditor({ + sourcesRoot: path.join(root, 'src'), + entryPoints: [ + 'vs/editor/editor.main', + 'vs/editor/editor.worker', + 'vs/base/worker/workerMain', + ], + inlineEntryPoints: [ + apiusages, + extrausages + ], + libs: [ + `lib.d.ts`, + `lib.es2015.collection.d.ts` + ], + redirects: { + 'vs/base/browser/ui/octiconLabel/octiconLabel': 'vs/base/browser/ui/octiconLabel/octiconLabel.mock', + }, + compilerOptions: { + module: 2, // ModuleKind.AMD + }, + shakeLevel: 2, // 0-Files, 1-InnerFile, 2-ClassMembers + importIgnorePattern: /^vs\/css!/, + destRoot: path.join(root, 'out-editor-src') + }); +}); + +// Full compile, including nls and inline sources in sourcemaps, for build +gulp.task('clean-editor-build', util.rimraf('out-editor-build')); +gulp.task('compile-editor-build', ['clean-editor-build', 'extract-editor-src'], compilation.compileTask('out-editor-src', 'out-editor-build', true)); + gulp.task('clean-optimized-editor', util.rimraf('out-editor')); -gulp.task('optimize-editor', ['clean-optimized-editor', 'compile-client-build'], common.optimizeTask({ +gulp.task('optimize-editor', ['clean-optimized-editor', 'compile-editor-build'], common.optimizeTask({ + src: 'out-editor-build', entryPoints: editorEntryPoints, otherSources: editorOtherSources, resources: editorResources, - loaderConfig: editorLoaderConfig(), + loaderConfig: { + paths: { + 'vs': 'out-editor-build/vs', + 'vscode': 'empty:' + } + }, bundleLoader: false, header: BUNDLED_FILE_HEADER, bundleInfo: true, @@ -230,7 +259,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..090db00ffeb 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 = [ @@ -80,7 +78,6 @@ const vscodeResources = [ 'out-build/vs/workbench/parts/webview/electron-browser/webview-pre.js', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/parts/tasks/**/*.json', - 'out-build/vs/workbench/parts/terminal/electron-browser/terminalProcess.js', 'out-build/vs/workbench/parts/welcome/walkThrough/**/*.md', 'out-build/vs/workbench/services/files/**/*.exe', 'out-build/vs/workbench/services/files/**/*.md', @@ -98,6 +95,7 @@ const BUNDLED_FILE_HEADER = [ gulp.task('clean-optimized-vscode', util.rimraf('out-vscode')); gulp.task('optimize-vscode', ['clean-optimized-vscode', 'compile-build', 'compile-extensions-build'], common.optimizeTask({ + src: 'out-build', entryPoints: vscodeEntryPoints, otherSources: [], resources: vscodeResources, @@ -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/compilation.js b/build/lib/compilation.js index 998ebb4f379..ad73e2dcf73 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -18,18 +18,21 @@ var _ = require("underscore"); var monacodts = require("../monaco/api"); var fs = require("fs"); var reporter = reporter_1.createReporter(); -var rootDir = path.join(__dirname, '../../src'); -var options = require('../../src/tsconfig.json').compilerOptions; -options.verbose = false; -options.sourceMap = true; -if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry - options.sourceMap = false; +function getTypeScriptCompilerOptions(src) { + var rootDir = path.join(__dirname, "../../" + src); + var options = require("../../" + src + "/tsconfig.json").compilerOptions; + options.verbose = false; + options.sourceMap = true; + if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry + options.sourceMap = false; + } + options.rootDir = rootDir; + options.sourceRoot = util.toFileUri(rootDir); + options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; + return options; } -options.rootDir = rootDir; -options.sourceRoot = util.toFileUri(rootDir); -options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; -function createCompile(build, emitError) { - var opts = _.clone(options); +function createCompile(src, build, emitError) { + var opts = _.clone(getTypeScriptCompilerOptions(src)); opts.inlineSources = !!build; opts.noFilesystemLookup = true; var ts = tsb.create(opts, null, null, function (err) { return reporter(err.toString()); }); @@ -51,31 +54,31 @@ function createCompile(build, emitError) { .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: options.sourceRoot + sourceRoot: opts.sourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); return es.duplex(input, output); }; } -function compileTask(out, build) { +function compileTask(src, out, build) { return function () { - var compile = createCompile(build, true); - var src = es.merge(gulp.src('src/**', { base: 'src' }), gulp.src('node_modules/typescript/lib/lib.d.ts')); + var compile = createCompile(src, build, true); + var srcPipe = es.merge(gulp.src(src + "/**", { base: "" + src }), gulp.src('node_modules/typescript/lib/lib.d.ts')); // Do not write .d.ts files to disk, as they are not needed there. var dtsFilter = util.filter(function (data) { return !/\.d\.ts$/.test(data.path); }); - return src + return srcPipe .pipe(compile()) .pipe(dtsFilter) .pipe(gulp.dest(out)) .pipe(dtsFilter.restore) - .pipe(monacodtsTask(out, false)); + .pipe(src !== 'src' ? es.through() : monacodtsTask(out, false)); }; } exports.compileTask = compileTask; function watchTask(out, build) { return function () { - var compile = createCompile(build); + var compile = createCompile('src', build); var src = es.merge(gulp.src('src/**', { base: 'src' }), gulp.src('node_modules/typescript/lib/lib.d.ts')); var watchSrc = watch('src/**', { base: 'src' }); // Do not write .d.ts files to disk, as they are not needed there. @@ -122,6 +125,7 @@ function monacodtsTask(out, isWatch) { fs.writeFileSync(result.filePath, result.content); } else { + fs.writeFileSync(result.filePath, result.content); resultStream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); } } diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index cedcb4155b6..33d8c111690 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -21,19 +21,22 @@ import * as fs from 'fs'; const reporter = createReporter(); -const rootDir = path.join(__dirname, '../../src'); -const options = require('../../src/tsconfig.json').compilerOptions; -options.verbose = false; -options.sourceMap = true; -if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry - options.sourceMap = false; +function getTypeScriptCompilerOptions(src: string) { + const rootDir = path.join(__dirname, `../../${src}`); + const options = require(`../../${src}/tsconfig.json`).compilerOptions; + options.verbose = false; + options.sourceMap = true; + if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry + options.sourceMap = false; + } + options.rootDir = rootDir; + options.sourceRoot = util.toFileUri(rootDir); + options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; + return options; } -options.rootDir = rootDir; -options.sourceRoot = util.toFileUri(rootDir); -options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; -function createCompile(build: boolean, emitError?: boolean): (token?: util.ICancellationToken) => NodeJS.ReadWriteStream { - const opts = _.clone(options); +function createCompile(src: string, build: boolean, emitError?: boolean): (token?: util.ICancellationToken) => NodeJS.ReadWriteStream { + const opts = _.clone(getTypeScriptCompilerOptions(src)); opts.inlineSources = !!build; opts.noFilesystemLookup = true; @@ -59,7 +62,7 @@ function createCompile(build: boolean, emitError?: boolean): (token?: util.ICanc .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: options.sourceRoot + sourceRoot: opts.sourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); @@ -68,32 +71,32 @@ function createCompile(build: boolean, emitError?: boolean): (token?: util.ICanc }; } -export function compileTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { +export function compileTask(src: string, out: string, build: boolean): () => NodeJS.ReadWriteStream { return function () { - const compile = createCompile(build, true); + const compile = createCompile(src, build, true); - const src = es.merge( - gulp.src('src/**', { base: 'src' }), + const srcPipe = es.merge( + gulp.src(`${src}/**`, { base: `${src}` }), gulp.src('node_modules/typescript/lib/lib.d.ts'), ); // Do not write .d.ts files to disk, as they are not needed there. const dtsFilter = util.filter(data => !/\.d\.ts$/.test(data.path)); - return src + return srcPipe .pipe(compile()) .pipe(dtsFilter) .pipe(gulp.dest(out)) .pipe(dtsFilter.restore) - .pipe(monacodtsTask(out, false)); + .pipe(src !== 'src' ? es.through() : monacodtsTask(out, false)); }; } export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { return function () { - const compile = createCompile(build); + const compile = createCompile('src', build); const src = es.merge( gulp.src('src/**', { base: 'src' }), @@ -150,6 +153,7 @@ function monacodtsTask(out: string, isWatch: boolean): NodeJS.ReadWriteStream { if (isWatch) { fs.writeFileSync(result.filePath, result.content); } else { + fs.writeFileSync(result.filePath, result.content); resultStream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); } } diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 3599086a4b4..4fafbd800a9 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -37,19 +37,19 @@ function loaderConfig(emptyPaths) { } exports.loaderConfig = loaderConfig; var IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; -function loader(bundledFileHeader, bundleLoader) { +function loader(src, bundledFileHeader, bundleLoader) { var sources = [ - 'out-build/vs/loader.js' + src + "/vs/loader.js" ]; if (bundleLoader) { sources = sources.concat([ - 'out-build/vs/css.js', - 'out-build/vs/nls.js' + src + "/vs/css.js", + src + "/vs/nls.js" ]); } var isFirst = true; return (gulp - .src(sources, { base: 'out-build' }) + .src(sources, { base: "" + src }) .pipe(es.through(function (data) { if (isFirst) { isFirst = false; @@ -71,7 +71,7 @@ function loader(bundledFileHeader, bundleLoader) { return f; }))); } -function toConcatStream(bundledFileHeader, sources, dest) { +function toConcatStream(src, bundledFileHeader, sources, dest) { var useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); // If a bundle ends up including in any of the sources our copyright, then // insert a fake source at the beginning of each bundle with our copyright @@ -91,7 +91,7 @@ function toConcatStream(bundledFileHeader, sources, dest) { } var treatedSources = sources.map(function (source) { var root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; - var base = source.path ? root + '/out-build' : ''; + var base = source.path ? root + ("/" + src) : ''; return new VinylFile({ path: source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake', base: base, @@ -102,12 +102,13 @@ function toConcatStream(bundledFileHeader, sources, dest) { .pipe(useSourcemaps ? util.loadSourcemaps() : es.through()) .pipe(concat(dest)); } -function toBundleStream(bundledFileHeader, bundles) { +function toBundleStream(src, bundledFileHeader, bundles) { return es.merge(bundles.map(function (bundle) { - return toConcatStream(bundledFileHeader, bundle.sources, bundle.dest); + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest); })); } function optimizeTask(opts) { + var src = opts.src; var entryPoints = opts.entryPoints; var otherSources = opts.otherSources; var resources = opts.resources; @@ -123,7 +124,7 @@ function optimizeTask(opts) { if (err) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(bundledFileHeader, result.files).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files).pipe(bundlesStream); // Remove css inlined resources var filteredResources = resources.slice(); result.cssInlinedResources.forEach(function (resource) { @@ -132,7 +133,7 @@ function optimizeTask(opts) { } filteredResources.push('!' + resource); }); - gulp.src(filteredResources, { base: 'out-build' }).pipe(resourcesStream); + gulp.src(filteredResources, { base: "" + src }).pipe(resourcesStream); var bundleInfoArray = []; if (opts.bundleInfo) { bundleInfoArray.push(new VinylFile({ @@ -145,9 +146,9 @@ function optimizeTask(opts) { }); var otherSourcesStream = es.through(); var otherSourcesStreamArr = []; - gulp.src(otherSources, { base: 'out-build' }) + gulp.src(otherSources, { base: "" + src }) .pipe(es.through(function (data) { - otherSourcesStreamArr.push(toConcatStream(bundledFileHeader, [data], data.relative)); + otherSourcesStreamArr.push(toConcatStream(src, bundledFileHeader, [data], data.relative)); }, function () { if (!otherSourcesStreamArr.length) { setTimeout(function () { otherSourcesStream.emit('end'); }, 0); @@ -156,7 +157,7 @@ function optimizeTask(opts) { es.merge(otherSourcesStreamArr).pipe(otherSourcesStream); } })); - var result = es.merge(loader(bundledFileHeader, bundleLoader), bundlesStream, otherSourcesStream, resourcesStream, bundleInfoStream); + var result = es.merge(loader(src, bundledFileHeader, bundleLoader), bundlesStream, otherSourcesStream, resourcesStream, bundleInfoStream); return result .pipe(sourcemaps.write('./', { sourceRoot: null, diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 7c5f15309c3..78328e3ce2c 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, '../..'); @@ -49,21 +50,21 @@ declare class FileSourceMap extends VinylFile { public sourceMap: sm.RawSourceMap; } -function loader(bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWriteStream { +function loader(src: string, bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWriteStream { let sources = [ - 'out-build/vs/loader.js' + `${src}/vs/loader.js` ]; if (bundleLoader) { sources = sources.concat([ - 'out-build/vs/css.js', - 'out-build/vs/nls.js' + `${src}/vs/css.js`, + `${src}/vs/nls.js` ]); } let isFirst = true; return ( gulp - .src(sources, { base: 'out-build' }) + .src(sources, { base: `${src}` }) .pipe(es.through(function (data) { if (isFirst) { isFirst = false; @@ -86,7 +87,7 @@ function loader(bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWr ); } -function toConcatStream(bundledFileHeader: string, sources: bundle.IFile[], dest: string): NodeJS.ReadWriteStream { +function toConcatStream(src: string, bundledFileHeader: string, sources: bundle.IFile[], dest: string): NodeJS.ReadWriteStream { const useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); // If a bundle ends up including in any of the sources our copyright, then @@ -109,7 +110,7 @@ function toConcatStream(bundledFileHeader: string, sources: bundle.IFile[], dest const treatedSources = sources.map(function (source) { const root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; - const base = source.path ? root + '/out-build' : ''; + const base = source.path ? root + `/${src}` : ''; return new VinylFile({ path: source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake', @@ -123,13 +124,17 @@ function toConcatStream(bundledFileHeader: string, sources: bundle.IFile[], dest .pipe(concat(dest)); } -function toBundleStream(bundledFileHeader: string, bundles: bundle.IConcatFile[]): NodeJS.ReadWriteStream { +function toBundleStream(src:string, bundledFileHeader: string, bundles: bundle.IConcatFile[]): NodeJS.ReadWriteStream { return es.merge(bundles.map(function (bundle) { - return toConcatStream(bundledFileHeader, bundle.sources, bundle.dest); + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest); })); } export interface IOptimizeTaskOpts { + /** + * The folder to read files from. + */ + src: string; /** * (for AMD files, will get bundled and get Copyright treatment) */ @@ -159,9 +164,14 @@ export interface IOptimizeTaskOpts { * (out folder name) */ out: string; + /** + * (out folder name) + */ + languages?: Language[]; } export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStream { + const src = opts.src; const entryPoints = opts.entryPoints; const otherSources = opts.otherSources; const resources = opts.resources; @@ -178,7 +188,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr bundle.bundle(entryPoints, loaderConfig, function (err, result) { if (err) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(bundledFileHeader, result.files).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files).pipe(bundlesStream); // Remove css inlined resources const filteredResources = resources.slice(); @@ -188,7 +198,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr } filteredResources.push('!' + resource); }); - gulp.src(filteredResources, { base: 'out-build' }).pipe(resourcesStream); + gulp.src(filteredResources, { base: `${src}` }).pipe(resourcesStream); const bundleInfoArray: VinylFile[] = []; if (opts.bundleInfo) { @@ -204,9 +214,9 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr const otherSourcesStream = es.through(); const otherSourcesStreamArr: NodeJS.ReadWriteStream[] = []; - gulp.src(otherSources, { base: 'out-build' }) + gulp.src(otherSources, { base: `${src}` }) .pipe(es.through(function (data) { - otherSourcesStreamArr.push(toConcatStream(bundledFileHeader, [data], data.relative)); + otherSourcesStreamArr.push(toConcatStream(src, bundledFileHeader, [data], data.relative)); }, function () { if (!otherSourcesStreamArr.length) { setTimeout(function () { otherSourcesStream.emit('end'); }, 0); @@ -216,7 +226,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr })); const result = es.merge( - loader(bundledFileHeader, bundleLoader), + loader(src, bundledFileHeader, bundleLoader), bundlesStream, otherSourcesStream, resourcesStream, diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 12511b01d36..885d3e789f5 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -7,9 +7,93 @@ Object.defineProperty(exports, "__esModule", { value: true }); var ts = require("typescript"); var fs = require("fs"); var path = require("path"); +var tss = require("./treeshaking"); var REPO_ROOT = path.join(__dirname, '../../'); var SRC_DIR = path.join(REPO_ROOT, 'src'); var OUT_EDITOR = path.join(REPO_ROOT, 'out-editor'); +var dirCache = {}; +function writeFile(filePath, contents) { + function ensureDirs(dirPath) { + if (dirCache[dirPath]) { + return; + } + dirCache[dirPath] = true; + ensureDirs(path.dirname(dirPath)); + if (fs.existsSync(dirPath)) { + return; + } + fs.mkdirSync(dirPath); + } + ensureDirs(path.dirname(filePath)); + fs.writeFileSync(filePath, contents); +} +function extractEditor(options) { + var result = tss.shake(options); + for (var fileName in result) { + if (result.hasOwnProperty(fileName)) { + writeFile(path.join(options.destRoot, fileName), result[fileName]); + } + } + var copied = {}; + var copyFile = function (fileName) { + if (copied[fileName]) { + return; + } + copied[fileName] = true; + var srcPath = path.join(options.sourcesRoot, fileName); + var dstPath = path.join(options.destRoot, fileName); + writeFile(dstPath, fs.readFileSync(srcPath)); + }; + var writeOutputFile = function (fileName, contents) { + writeFile(path.join(options.destRoot, fileName), contents); + }; + for (var fileName in result) { + if (result.hasOwnProperty(fileName)) { + var fileContents = result[fileName]; + var info = ts.preProcessFile(fileContents); + for (var i = info.importedFiles.length - 1; i >= 0; i--) { + var importedFileName = info.importedFiles[i].fileName; + var importedFilePath = void 0; + if (/^vs\/css!/.test(importedFileName)) { + importedFilePath = importedFileName.substr('vs/css!'.length) + '.css'; + } + else { + importedFilePath = importedFileName; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { + importedFilePath = path.join(path.dirname(fileName), importedFilePath); + } + if (/\.css$/.test(importedFilePath)) { + transportCSS(importedFilePath, copyFile, writeOutputFile); + } + else { + if (fs.existsSync(path.join(options.sourcesRoot, importedFilePath + '.js'))) { + copyFile(importedFilePath + '.js'); + } + } + } + } + } + var tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.json')).toString()); + tsConfig.compilerOptions.noUnusedLocals = false; + writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); + [ + 'vs/css.build.js', + 'vs/css.d.ts', + 'vs/css.js', + 'vs/loader.js', + 'vs/monaco.d.ts', + 'vs/nls.build.js', + 'vs/nls.d.ts', + 'vs/nls.js', + 'vs/nls.mock.ts', + 'typings/lib.ie11_safe_es6.d.ts', + 'typings/thenable.d.ts', + 'typings/es6-promise.d.ts', + 'typings/require.d.ts', + ].forEach(copyFile); +} +exports.extractEditor = extractEditor; function createESMSourcesAndResources(options) { var OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); var OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); @@ -94,7 +178,7 @@ function createESMSourcesAndResources(options) { options.entryPoints.forEach(function (entryPoint) { return enqueue(entryPoint); }); while (queue.length > 0) { var module_1 = queue.shift(); - if (transportCSS(options, module_1, enqueue, write)) { + if (transportCSS(module_1, enqueue, write)) { continue; } if (transportResource(options, module_1, enqueue, write)) { @@ -171,7 +255,7 @@ function createESMSourcesAndResources(options) { fs.writeFileSync(path.join(OUT_FOLDER, 'vs/monaco.d.ts'), monacodts); } exports.createESMSourcesAndResources = createESMSourcesAndResources; -function transportCSS(options, module, enqueue, write) { +function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; } @@ -179,10 +263,10 @@ function transportCSS(options, module, enqueue, write) { var fileContents = fs.readFileSync(filename).toString(); var inlineResources = 'base64'; // see https://github.com/Microsoft/monaco-editor/issues/148 var inlineResourcesLimit = 300000; //3000; // see https://github.com/Microsoft/monaco-editor/issues/336 - var newContents = _rewriteOrInlineUrls(filename, fileContents, inlineResources === 'base64', inlineResourcesLimit); + var newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64', inlineResourcesLimit); write(module, newContents); return true; - function _rewriteOrInlineUrls(originalFileFSPath, contents, forceBase64, inlineByteLimit) { + function _rewriteOrInlineUrls(contents, forceBase64, inlineByteLimit) { return _replaceURL(contents, function (url) { var imagePath = path.join(path.dirname(module), url); var fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index a402cf68405..7931737fadb 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -6,11 +6,101 @@ import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; +import * as tss from './treeshaking'; const REPO_ROOT = path.join(__dirname, '../../'); const SRC_DIR = path.join(REPO_ROOT, 'src'); const OUT_EDITOR = path.join(REPO_ROOT, 'out-editor'); +let dirCache: { [dir: string]: boolean; } = {}; + +function writeFile(filePath: string, contents: Buffer | string): void { + function ensureDirs(dirPath: string): void { + if (dirCache[dirPath]) { + return; + } + dirCache[dirPath] = true; + + ensureDirs(path.dirname(dirPath)); + if (fs.existsSync(dirPath)) { + return; + } + fs.mkdirSync(dirPath); + } + ensureDirs(path.dirname(filePath)); + fs.writeFileSync(filePath, contents); +} + +export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string }): void { + let result = tss.shake(options); + for (let fileName in result) { + if (result.hasOwnProperty(fileName)) { + writeFile(path.join(options.destRoot, fileName), result[fileName]); + } + } + let copied: { [fileName:string]: boolean; } = {}; + const copyFile = (fileName: string) => { + if (copied[fileName]) { + return; + } + copied[fileName] = true; + const srcPath = path.join(options.sourcesRoot, fileName); + const dstPath = path.join(options.destRoot, fileName); + writeFile(dstPath, fs.readFileSync(srcPath)); + }; + const writeOutputFile = (fileName: string, contents: string) => { + writeFile(path.join(options.destRoot, fileName), contents); + }; + for (let fileName in result) { + if (result.hasOwnProperty(fileName)) { + const fileContents = result[fileName]; + const info = ts.preProcessFile(fileContents); + + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFileName = info.importedFiles[i].fileName; + + let importedFilePath: string; + if (/^vs\/css!/.test(importedFileName)) { + importedFilePath = importedFileName.substr('vs/css!'.length) + '.css'; + } else { + importedFilePath = importedFileName; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { + importedFilePath = path.join(path.dirname(fileName), importedFilePath); + } + + if (/\.css$/.test(importedFilePath)) { + transportCSS(importedFilePath, copyFile, writeOutputFile); + } else { + if (fs.existsSync(path.join(options.sourcesRoot, importedFilePath + '.js'))) { + copyFile(importedFilePath + '.js'); + } + } + } + } + } + + const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.json')).toString()); + tsConfig.compilerOptions.noUnusedLocals = false; + writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); + + [ + 'vs/css.build.js', + 'vs/css.d.ts', + 'vs/css.js', + 'vs/loader.js', + 'vs/monaco.d.ts', + 'vs/nls.build.js', + 'vs/nls.d.ts', + 'vs/nls.js', + 'vs/nls.mock.ts', + 'typings/lib.ie11_safe_es6.d.ts', + 'typings/thenable.d.ts', + 'typings/es6-promise.d.ts', + 'typings/require.d.ts', + ].forEach(copyFile); +} + export interface IOptions { entryPoints: string[]; outFolder: string; @@ -111,7 +201,7 @@ export function createESMSourcesAndResources(options: IOptions): void { while (queue.length > 0) { const module = queue.shift(); - if (transportCSS(options, module, enqueue, write)) { + if (transportCSS(module, enqueue, write)) { continue; } if (transportResource(options, module, enqueue, write)) { @@ -198,7 +288,7 @@ export function createESMSourcesAndResources(options: IOptions): void { } -function transportCSS(options: IOptions, module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { +function transportCSS(module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { if (!/\.css/.test(module)) { return false; @@ -209,11 +299,11 @@ function transportCSS(options: IOptions, module: string, enqueue: (module: strin const inlineResources = 'base64'; // see https://github.com/Microsoft/monaco-editor/issues/148 const inlineResourcesLimit = 300000;//3000; // see https://github.com/Microsoft/monaco-editor/issues/336 - const newContents = _rewriteOrInlineUrls(filename, fileContents, inlineResources === 'base64', inlineResourcesLimit); + const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64', inlineResourcesLimit); write(module, newContents); return true; - function _rewriteOrInlineUrls(originalFileFSPath: string, contents: string, forceBase64: boolean, inlineByteLimit: number): string { + function _rewriteOrInlineUrls(contents: string, forceBase64: boolean, inlineByteLimit: number): string { return _replaceURL(contents, (url) => { let imagePath = path.join(path.dirname(module), url); let fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); 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/treeshaking.js b/build/lib/treeshaking.js new file mode 100644 index 00000000000..c77fa128912 --- /dev/null +++ b/build/lib/treeshaking.js @@ -0,0 +1,682 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +var fs = require("fs"); +var path = require("path"); +var ts = require("typescript"); +var TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); +var ShakeLevel; +(function (ShakeLevel) { + ShakeLevel[ShakeLevel["Files"] = 0] = "Files"; + ShakeLevel[ShakeLevel["InnerFile"] = 1] = "InnerFile"; + ShakeLevel[ShakeLevel["ClassMembers"] = 2] = "ClassMembers"; +})(ShakeLevel = exports.ShakeLevel || (exports.ShakeLevel = {})); +function shake(options) { + var languageService = createTypeScriptLanguageService(options); + markNodes(languageService, options); + return generateResult(languageService, options.shakeLevel); +} +exports.shake = shake; +//#region Discovery, LanguageService & Setup +function createTypeScriptLanguageService(options) { + // Discover referenced files + var FILES = discoverAndReadFiles(options); + // Add fake usage files + options.inlineEntryPoints.forEach(function (inlineEntryPoint, index) { + FILES["inlineEntryPoint:" + index + ".ts"] = inlineEntryPoint; + }); + // Resolve libs + var RESOLVED_LIBS = {}; + options.libs.forEach(function (filename) { + var filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + RESOLVED_LIBS["defaultLib:" + filename] = fs.readFileSync(filepath).toString(); + }); + var host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, options.compilerOptions); + return ts.createLanguageService(host); +} +/** + * Read imports and follow them until all files have been handled + */ +function discoverAndReadFiles(options) { + var FILES = {}; + var in_queue = Object.create(null); + var queue = []; + var enqueue = function (moduleId) { + if (in_queue[moduleId]) { + return; + } + in_queue[moduleId] = true; + queue.push(moduleId); + }; + options.entryPoints.forEach(function (entryPoint) { return enqueue(entryPoint); }); + while (queue.length > 0) { + var moduleId = queue.shift(); + var dts_filename = path.join(options.sourcesRoot, moduleId + '.d.ts'); + if (fs.existsSync(dts_filename)) { + var dts_filecontents = fs.readFileSync(dts_filename).toString(); + FILES[moduleId + '.d.ts'] = dts_filecontents; + continue; + } + var ts_filename = void 0; + if (options.redirects[moduleId]) { + ts_filename = path.join(options.sourcesRoot, options.redirects[moduleId] + '.ts'); + } + else { + ts_filename = path.join(options.sourcesRoot, moduleId + '.ts'); + } + var ts_filecontents = fs.readFileSync(ts_filename).toString(); + var info = ts.preProcessFile(ts_filecontents); + for (var i = info.importedFiles.length - 1; i >= 0; i--) { + var importedFileName = info.importedFiles[i].fileName; + if (options.importIgnorePattern.test(importedFileName)) { + // Ignore vs/css! imports + continue; + } + var importedModuleId = importedFileName; + if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { + importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + } + enqueue(importedModuleId); + } + FILES[moduleId + '.ts'] = ts_filecontents; + } + return FILES; +} +/** + * A TypeScript language service host + */ +var TypeScriptLanguageServiceHost = /** @class */ (function () { + function TypeScriptLanguageServiceHost(libs, files, compilerOptions) { + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + // --- language service host --------------- + TypeScriptLanguageServiceHost.prototype.getCompilationSettings = function () { + return this._compilerOptions; + }; + TypeScriptLanguageServiceHost.prototype.getScriptFileNames = function () { + return ([] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files))); + }; + TypeScriptLanguageServiceHost.prototype.getScriptVersion = function (fileName) { + return '1'; + }; + TypeScriptLanguageServiceHost.prototype.getProjectVersion = function () { + return '1'; + }; + TypeScriptLanguageServiceHost.prototype.getScriptSnapshot = function (fileName) { + if (this._files.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._files[fileName]); + } + else if (this._libs.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._libs[fileName]); + } + else { + return ts.ScriptSnapshot.fromString(''); + } + }; + TypeScriptLanguageServiceHost.prototype.getScriptKind = function (fileName) { + return ts.ScriptKind.TS; + }; + TypeScriptLanguageServiceHost.prototype.getCurrentDirectory = function () { + return ''; + }; + TypeScriptLanguageServiceHost.prototype.getDefaultLibFileName = function (options) { + return 'defaultLib:lib.d.ts'; + }; + TypeScriptLanguageServiceHost.prototype.isDefaultLibFileName = function (fileName) { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + }; + return TypeScriptLanguageServiceHost; +}()); +//#endregion +//#region Tree Shaking +var NodeColor; +(function (NodeColor) { + NodeColor[NodeColor["White"] = 0] = "White"; + NodeColor[NodeColor["Gray"] = 1] = "Gray"; + NodeColor[NodeColor["Black"] = 2] = "Black"; +})(NodeColor || (NodeColor = {})); +function getColor(node) { + return node.$$$color || 0 /* White */; +} +function setColor(node, color) { + node.$$$color = color; +} +function nodeOrParentIsBlack(node) { + while (node) { + var color = getColor(node); + if (color === 2 /* Black */) { + return true; + } + node = node.parent; + } + return false; +} +function nodeOrChildIsBlack(node) { + if (getColor(node) === 2 /* Black */) { + return true; + } + for (var _i = 0, _a = node.getChildren(); _i < _a.length; _i++) { + var child = _a[_i]; + if (nodeOrChildIsBlack(child)) { + return true; + } + } + return false; +} +function markNodes(languageService, options) { + var program = languageService.getProgram(); + if (options.shakeLevel === 0 /* Files */) { + // Mark all source files Black + program.getSourceFiles().forEach(function (sourceFile) { + setColor(sourceFile, 2 /* Black */); + }); + return; + } + var black_queue = []; + var gray_queue = []; + var sourceFilesLoaded = {}; + function enqueueTopLevelModuleStatements(sourceFile) { + sourceFile.forEachChild(function (node) { + if (ts.isImportDeclaration(node)) { + if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, 2 /* Black */); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + if (ts.isExportDeclaration(node)) { + if (ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, 2 /* Black */); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + if (ts.isExpressionStatement(node) + || ts.isIfStatement(node) + || ts.isIterationStatement(node, true) + || ts.isExportAssignment(node)) { + enqueue_black(node); + } + if (ts.isImportEqualsDeclaration(node)) { + if (/export/.test(node.getFullText(sourceFile))) { + // e.g. "export import Severity = BaseSeverity;" + enqueue_black(node); + } + } + }); + } + function enqueue_gray(node) { + if (nodeOrParentIsBlack(node) || getColor(node) === 1 /* Gray */) { + return; + } + setColor(node, 1 /* Gray */); + gray_queue.push(node); + } + function enqueue_black(node) { + var previousColor = getColor(node); + if (previousColor === 2 /* Black */) { + return; + } + if (previousColor === 1 /* Gray */) { + // remove from gray queue + gray_queue.splice(gray_queue.indexOf(node), 1); + setColor(node, 0 /* White */); + // add to black queue + enqueue_black(node); + // // move from one queue to the other + // black_queue.push(node); + // setColor(node, NodeColor.Black); + return; + } + if (nodeOrParentIsBlack(node)) { + return; + } + var fileName = node.getSourceFile().fileName; + if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { + setColor(node, 2 /* Black */); + return; + } + var sourceFile = node.getSourceFile(); + if (!sourceFilesLoaded[sourceFile.fileName]) { + sourceFilesLoaded[sourceFile.fileName] = true; + enqueueTopLevelModuleStatements(sourceFile); + } + if (ts.isSourceFile(node)) { + return; + } + setColor(node, 2 /* Black */); + black_queue.push(node); + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + var references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); + if (references) { + for (var i = 0, len = references.length; i < len; i++) { + var reference = references[i]; + var referenceSourceFile = program.getSourceFile(reference.fileName); + var referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + if (ts.isMethodDeclaration(referenceNode.parent) + || ts.isPropertyDeclaration(referenceNode.parent) + || ts.isGetAccessor(referenceNode.parent) + || ts.isSetAccessor(referenceNode.parent)) { + enqueue_gray(referenceNode.parent); + } + } + } + } + } + function enqueueFile(filename) { + var sourceFile = program.getSourceFile(filename); + if (!sourceFile) { + console.warn("Cannot find source file " + filename); + return; + } + enqueue_black(sourceFile); + } + function enqueueImport(node, importText) { + if (options.importIgnorePattern.test(importText)) { + // this import should be ignored + return; + } + var nodeSourceFile = node.getSourceFile(); + var fullPath; + if (/(^\.\/)|(^\.\.\/)/.test(importText)) { + fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + } + else { + fullPath = importText + '.ts'; + } + enqueueFile(fullPath); + } + options.entryPoints.forEach(function (moduleId) { return enqueueFile(moduleId + '.ts'); }); + // Add fake usage files + options.inlineEntryPoints.forEach(function (_, index) { return enqueueFile("inlineEntryPoint:" + index + ".ts"); }); + var step = 0; + var checker = program.getTypeChecker(); + var _loop_1 = function () { + ++step; + var node = void 0; + if (step % 100 === 0) { + console.log(step + "/" + (step + black_queue.length + gray_queue.length) + " (" + black_queue.length + ", " + gray_queue.length + ")"); + } + if (black_queue.length === 0) { + for (var i = 0; i < gray_queue.length; i++) { + var node_1 = gray_queue[i]; + var nodeParent = node_1.parent; + if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { + gray_queue.splice(i, 1); + black_queue.push(node_1); + setColor(node_1, 2 /* Black */); + i--; + } + } + } + if (black_queue.length > 0) { + node = black_queue.shift(); + } + else { + return "break"; + } + var nodeSourceFile = node.getSourceFile(); + var loop = function (node) { + var _a = getRealNodeSymbol(checker, node), symbol = _a[0], symbolImportNode = _a[1]; + if (symbolImportNode) { + setColor(symbolImportNode, 2 /* Black */); + } + if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + for (var i = 0, len = symbol.declarations.length; i < len; i++) { + var declaration = symbol.declarations[i]; + if (ts.isSourceFile(declaration)) { + // Do not enqueue full source files + // (they can be the declaration of a module import) + continue; + } + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + enqueue_black(declaration.name); + for (var j = 0; j < declaration.members.length; j++) { + var member = declaration.members[j]; + var memberName = member.name ? member.name.getText() : null; + if (ts.isConstructorDeclaration(member) + || ts.isConstructSignatureDeclaration(member) + || ts.isIndexSignatureDeclaration(member) + || ts.isCallSignatureDeclaration(member) + || memberName === 'toJSON' + || memberName === 'toString' + || memberName === 'dispose' // TODO: keeping all `dispose` methods + ) { + enqueue_black(member); + } + } + // queue the heritage clauses + if (declaration.heritageClauses) { + for (var _i = 0, _b = declaration.heritageClauses; _i < _b.length; _i++) { + var heritageClause = _b[_i]; + enqueue_black(heritageClause); + } + } + } + else { + enqueue_black(declaration); + } + } + } + node.forEachChild(loop); + }; + node.forEachChild(loop); + }; + while (black_queue.length > 0 || gray_queue.length > 0) { + var state_1 = _loop_1(); + if (state_1 === "break") + break; + } +} +function nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol) { + for (var i = 0, len = symbol.declarations.length; i < len; i++) { + var declaration = symbol.declarations[i]; + var declarationSourceFile = declaration.getSourceFile(); + if (nodeSourceFile === declarationSourceFile) { + if (declaration.pos <= node.pos && node.end <= declaration.end) { + return true; + } + } + } + return false; +} +function generateResult(languageService, shakeLevel) { + var program = languageService.getProgram(); + var result = {}; + var writeFile = function (filePath, contents) { + result[filePath] = contents; + }; + program.getSourceFiles().forEach(function (sourceFile) { + var fileName = sourceFile.fileName; + if (/^defaultLib:/.test(fileName)) { + return; + } + var destination = fileName; + if (/\.d\.ts$/.test(fileName)) { + if (nodeOrChildIsBlack(sourceFile)) { + writeFile(destination, sourceFile.text); + } + return; + } + var text = sourceFile.text; + var result = ''; + function keep(node) { + result += text.substring(node.pos, node.end); + } + function write(data) { + result += data; + } + function writeMarkedNodes(node) { + if (getColor(node) === 2 /* Black */) { + return keep(node); + } + // Always keep certain top-level statements + if (ts.isSourceFile(node.parent)) { + if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') { + return keep(node); + } + if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) { + return keep(node); + } + } + // Keep the entire import in import * as X cases + if (ts.isImportDeclaration(node)) { + if (node.importClause && node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + if (getColor(node.importClause.namedBindings) === 2 /* Black */) { + return keep(node); + } + } + else { + var survivingImports = []; + for (var i = 0; i < node.importClause.namedBindings.elements.length; i++) { + var importNode = node.importClause.namedBindings.elements[i]; + if (getColor(importNode) === 2 /* Black */) { + survivingImports.push(importNode.getFullText(sourceFile)); + } + } + var leadingTriviaWidth = node.getLeadingTriviaWidth(); + var leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); + if (survivingImports.length > 0) { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return write(leadingTrivia + "import " + node.importClause.name.text + ", {" + survivingImports.join(',') + " } from" + node.moduleSpecifier.getFullText(sourceFile) + ";"); + } + return write(leadingTrivia + "import {" + survivingImports.join(',') + " } from" + node.moduleSpecifier.getFullText(sourceFile) + ";"); + } + else { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return write(leadingTrivia + "import " + node.importClause.name.text + " from" + node.moduleSpecifier.getFullText(sourceFile) + ";"); + } + } + } + } + else { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return keep(node); + } + } + } + if (shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { + var toWrite = node.getFullText(); + for (var i = node.members.length - 1; i >= 0; i--) { + var member = node.members[i]; + if (getColor(member) === 2 /* Black */) { + // keep method + continue; + } + if (/^_(.*)Brand$/.test(member.name.getText())) { + // TODO: keep all members ending with `Brand`... + continue; + } + var pos = member.pos - node.pos; + var end = member.end - node.pos; + toWrite = toWrite.substring(0, pos) + toWrite.substring(end); + } + return write(toWrite); + } + if (ts.isFunctionDeclaration(node)) { + // Do not go inside functions if they haven't been marked + return; + } + node.forEachChild(writeMarkedNodes); + } + if (getColor(sourceFile) !== 2 /* Black */) { + if (!nodeOrChildIsBlack(sourceFile)) { + // none of the elements are reachable => don't write this file at all! + return; + } + sourceFile.forEachChild(writeMarkedNodes); + result += sourceFile.endOfFileToken.getFullText(sourceFile); + } + else { + result = text; + } + writeFile(destination, result); + }); + return result; +} +//#endregion +//#region Utils +/** + * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) + */ +function getRealNodeSymbol(checker, node) { + /** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ + /* @internal */ + function getContainingObjectLiteralElement(node) { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + if (node.parent.kind === ts.SyntaxKind.ComputedPropertyName) { + return ts.isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; + } + // falls through + case ts.SyntaxKind.Identifier: + return ts.isObjectLiteralElement(node.parent) && + (node.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === ts.SyntaxKind.JsxAttributes) && + node.parent.name === node ? node.parent : undefined; + } + return undefined; + } + function getPropertySymbolsFromType(type, propName) { + function getTextOfPropertyName(name) { + function isStringOrNumericLiteral(node) { + var kind = node.kind; + return kind === ts.SyntaxKind.StringLiteral + || kind === ts.SyntaxKind.NumericLiteral; + } + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return name.text; + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + return name.text; + case ts.SyntaxKind.ComputedPropertyName: + return isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined; + } + } + var name = getTextOfPropertyName(propName); + if (name && type) { + var result = []; + var symbol_1 = type.getProperty(name); + if (type.flags & ts.TypeFlags.Union) { + for (var _i = 0, _a = type.types; _i < _a.length; _i++) { + var t = _a[_i]; + var symbol_2 = t.getProperty(name); + if (symbol_2) { + result.push(symbol_2); + } + } + return result; + } + if (symbol_1) { + result.push(symbol_1); + return result; + } + } + return undefined; + } + function getPropertySymbolsFromContextualType(typeChecker, node) { + var objectLiteral = node.parent; + var contextualType = typeChecker.getContextualType(objectLiteral); + return getPropertySymbolsFromType(contextualType, node.name); + } + // Go to the original declaration for cases: + // + // (1) when the aliased symbol was declared in the location(parent). + // (2) when the aliased symbol is originating from an import. + // + function shouldSkipAlias(node, declaration) { + if (node.kind !== ts.SyntaxKind.Identifier) { + return false; + } + if (node.parent === declaration) { + return true; + } + switch (declaration.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportEqualsDeclaration: + return true; + case ts.SyntaxKind.ImportSpecifier: + return declaration.parent.kind === ts.SyntaxKind.NamedImports; + default: + return false; + } + } + if (!ts.isShorthandPropertyAssignment(node)) { + if (node.getChildCount() !== 0) { + return [null, null]; + } + } + var symbol = checker.getSymbolAtLocation(node); + var importNode = null; + if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + var aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + // We should mark the import as visited + importNode = symbol.declarations[0]; + symbol = aliased; + } + } + if (symbol) { + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + } + // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the + // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern + // and return the property declaration for the referenced property. + // For example: + // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" + // + // function bar(onfulfilled: (value: T) => void) { //....} + // interface Test { + // pr/*destination*/op1: number + // } + // bar(({pr/*goto*/op1})=>{}); + if (ts.isPropertyName(node) && ts.isBindingElement(node.parent) && ts.isObjectBindingPattern(node.parent.parent) && + (node === (node.parent.propertyName || node.parent.name))) { + var type = checker.getTypeAtLocation(node.parent.parent); + if (type) { + var propSymbols = getPropertySymbolsFromType(type, node); + if (propSymbols) { + symbol = propSymbols[0]; + } + } + } + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /*first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: false }) + var element = getContainingObjectLiteralElement(node); + if (element && checker.getContextualType(element.parent)) { + var propertySymbols = getPropertySymbolsFromContextualType(checker, element); + if (propertySymbols) { + symbol = propertySymbols[0]; + } + } + } + if (symbol && symbol.declarations) { + return [symbol, importNode]; + } + return [null, null]; +} +/** Get the token whose text contains the position */ +function getTokenAtPosition(sourceFile, position, allowPositionInLeadingTrivia, includeEndPosition) { + var current = sourceFile; + outer: while (true) { + // find the child that contains 'position' + for (var _i = 0, _a = current.getChildren(); _i < _a.length; _i++) { + var child = _a[_i]; + var start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + break; + } + var end = child.getEnd(); + if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { + current = child; + continue outer; + } + } + return current; + } +} diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts new file mode 100644 index 00000000000..0527fe3ebce --- /dev/null +++ b/build/lib/treeshaking.ts @@ -0,0 +1,817 @@ +/*--------------------------------------------------------------------------------------------- + * 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 fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); + +export const enum ShakeLevel { + Files = 0, + InnerFile = 1, + ClassMembers = 2 +} + +export interface ITreeShakingOptions { + /** + * The full path to the root where sources are. + */ + sourcesRoot: string; + /** + * Module ids. + * e.g. `vs/editor/editor.main` or `index` + */ + entryPoints: string[]; + /** + * Inline usages. + */ + inlineEntryPoints: string[]; + /** + * TypeScript libs. + * e.g. `lib.d.ts`, `lib.es2015.collection.d.ts` + */ + libs: string[]; + /** + * TypeScript compiler options. + */ + compilerOptions: ts.CompilerOptions; + /** + * The shake level to perform. + */ + shakeLevel: ShakeLevel; + /** + * regex pattern to ignore certain imports e.g. `vs/css!` imports + */ + importIgnorePattern: RegExp; + + redirects: { [module: string]: string; }; +} + +export interface ITreeShakingResult { + [file: string]: string; +} + +export function shake(options: ITreeShakingOptions): ITreeShakingResult { + const languageService = createTypeScriptLanguageService(options); + + markNodes(languageService, options); + + return generateResult(languageService, options.shakeLevel); +} + +//#region Discovery, LanguageService & Setup +function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.LanguageService { + // Discover referenced files + const FILES = discoverAndReadFiles(options); + + // Add fake usage files + options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { + FILES[`inlineEntryPoint:${index}.ts`] = inlineEntryPoint; + }); + + // Resolve libs + const RESOLVED_LIBS: ILibMap = {}; + options.libs.forEach((filename) => { + const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + RESOLVED_LIBS[`defaultLib:${filename}`] = fs.readFileSync(filepath).toString(); + }); + + const host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, options.compilerOptions); + return ts.createLanguageService(host); +} + +/** + * Read imports and follow them until all files have been handled + */ +function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { + const FILES: IFileMap = {}; + + const in_queue: { [module: string]: boolean; } = Object.create(null); + const queue: string[] = []; + + const enqueue = (moduleId: string) => { + if (in_queue[moduleId]) { + return; + } + in_queue[moduleId] = true; + queue.push(moduleId); + }; + + options.entryPoints.forEach((entryPoint) => enqueue(entryPoint)); + + while (queue.length > 0) { + const moduleId = queue.shift(); + const dts_filename = path.join(options.sourcesRoot, moduleId + '.d.ts'); + if (fs.existsSync(dts_filename)) { + const dts_filecontents = fs.readFileSync(dts_filename).toString(); + FILES[moduleId + '.d.ts'] = dts_filecontents; + continue; + } + + let ts_filename: string; + if (options.redirects[moduleId]) { + ts_filename = path.join(options.sourcesRoot, options.redirects[moduleId] + '.ts'); + } else { + ts_filename = path.join(options.sourcesRoot, moduleId + '.ts'); + } + const ts_filecontents = fs.readFileSync(ts_filename).toString(); + const info = ts.preProcessFile(ts_filecontents); + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFileName = info.importedFiles[i].fileName; + + if (options.importIgnorePattern.test(importedFileName)) { + // Ignore vs/css! imports + continue; + } + + let importedModuleId = importedFileName; + if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { + importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + } + enqueue(importedModuleId); + } + + FILES[moduleId + '.ts'] = ts_filecontents; + } + + return FILES; +} + +interface ILibMap { [libName: string]: string; } +interface IFileMap { [fileName: string]: string; } + +/** + * A TypeScript language service host + */ +class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + + private readonly _libs: ILibMap; + private readonly _files: IFileMap; + private readonly _compilerOptions: ts.CompilerOptions; + + constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + + // --- language service host --------------- + + getCompilationSettings(): ts.CompilerOptions { + return this._compilerOptions; + } + getScriptFileNames(): string[] { + return ( + [] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files)) + ); + } + getScriptVersion(fileName: string): string { + return '1'; + } + getProjectVersion(): string { + return '1'; + } + getScriptSnapshot(fileName: string): ts.IScriptSnapshot { + if (this._files.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._files[fileName]); + } else if (this._libs.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._libs[fileName]); + } else { + return ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(fileName: string): ts.ScriptKind { + return ts.ScriptKind.TS; + } + getCurrentDirectory(): string { + return ''; + } + getDefaultLibFileName(options: ts.CompilerOptions): string { + return 'defaultLib:lib.d.ts'; + } + isDefaultLibFileName(fileName: string): boolean { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} +//#endregion + +//#region Tree Shaking + +const enum NodeColor { + White = 0, + Gray = 1, + Black = 2 +} + +function getColor(node: ts.Node): NodeColor { + return (node).$$$color || NodeColor.White; +} +function setColor(node: ts.Node, color: NodeColor): void { + (node).$$$color = color; +} +function nodeOrParentIsBlack(node: ts.Node): boolean { + while (node) { + const color = getColor(node); + if (color === NodeColor.Black) { + return true; + } + node = node.parent; + } + return false; +} +function nodeOrChildIsBlack(node: ts.Node): boolean { + if (getColor(node) === NodeColor.Black) { + return true; + } + for (const child of node.getChildren()) { + if (nodeOrChildIsBlack(child)) { + return true; + } + } + return false; +} + +function markNodes(languageService: ts.LanguageService, options: ITreeShakingOptions) { + const program = languageService.getProgram(); + + if (options.shakeLevel === ShakeLevel.Files) { + // Mark all source files Black + program.getSourceFiles().forEach((sourceFile) => { + setColor(sourceFile, NodeColor.Black); + }); + return; + } + + const black_queue: ts.Node[] = []; + const gray_queue: ts.Node[] = []; + const sourceFilesLoaded: { [fileName: string]: boolean } = {}; + + function enqueueTopLevelModuleStatements(sourceFile: ts.SourceFile): void { + + sourceFile.forEachChild((node: ts.Node) => { + + if (ts.isImportDeclaration(node)) { + if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, NodeColor.Black); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + + if (ts.isExportDeclaration(node)) { + if (ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, NodeColor.Black); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + + if ( + ts.isExpressionStatement(node) + || ts.isIfStatement(node) + || ts.isIterationStatement(node, true) + || ts.isExportAssignment(node) + ) { + enqueue_black(node); + } + + if (ts.isImportEqualsDeclaration(node)) { + if (/export/.test(node.getFullText(sourceFile))) { + // e.g. "export import Severity = BaseSeverity;" + enqueue_black(node); + } + } + + }); + } + + function enqueue_gray(node: ts.Node): void { + if (nodeOrParentIsBlack(node) || getColor(node) === NodeColor.Gray) { + return; + } + setColor(node, NodeColor.Gray); + gray_queue.push(node); + } + + function enqueue_black(node: ts.Node): void { + const previousColor = getColor(node); + + if (previousColor === NodeColor.Black) { + return; + } + + if (previousColor === NodeColor.Gray) { + // remove from gray queue + gray_queue.splice(gray_queue.indexOf(node), 1); + setColor(node, NodeColor.White); + + // add to black queue + enqueue_black(node); + + // // move from one queue to the other + // black_queue.push(node); + // setColor(node, NodeColor.Black); + return; + } + + if (nodeOrParentIsBlack(node)) { + return; + } + + const fileName = node.getSourceFile().fileName; + if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { + setColor(node, NodeColor.Black); + return; + } + + const sourceFile = node.getSourceFile(); + if (!sourceFilesLoaded[sourceFile.fileName]) { + sourceFilesLoaded[sourceFile.fileName] = true; + enqueueTopLevelModuleStatements(sourceFile); + } + + if (ts.isSourceFile(node)) { + return; + } + + setColor(node, NodeColor.Black); + black_queue.push(node); + + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); + if (references) { + for (let i = 0, len = references.length; i < len; i++) { + const reference = references[i]; + const referenceSourceFile = program.getSourceFile(reference.fileName); + const referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + if ( + ts.isMethodDeclaration(referenceNode.parent) + || ts.isPropertyDeclaration(referenceNode.parent) + || ts.isGetAccessor(referenceNode.parent) + || ts.isSetAccessor(referenceNode.parent) + ) { + enqueue_gray(referenceNode.parent); + } + } + } + } + } + + function enqueueFile(filename: string): void { + const sourceFile = program.getSourceFile(filename); + if (!sourceFile) { + console.warn(`Cannot find source file ${filename}`); + return; + } + enqueue_black(sourceFile); + } + + function enqueueImport(node: ts.Node, importText: string): void { + if (options.importIgnorePattern.test(importText)) { + // this import should be ignored + return; + } + + const nodeSourceFile = node.getSourceFile(); + let fullPath: string; + if (/(^\.\/)|(^\.\.\/)/.test(importText)) { + fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + } else { + fullPath = importText + '.ts'; + } + enqueueFile(fullPath); + } + + options.entryPoints.forEach(moduleId => enqueueFile(moduleId + '.ts')); + // Add fake usage files + options.inlineEntryPoints.forEach((_, index) => enqueueFile(`inlineEntryPoint:${index}.ts`)); + + let step = 0; + + const checker = program.getTypeChecker(); + while (black_queue.length > 0 || gray_queue.length > 0) { + ++step; + let node: ts.Node; + + if (step % 100 === 0) { + console.log(`${step}/${step+black_queue.length+gray_queue.length} (${black_queue.length}, ${gray_queue.length})`); + } + + if (black_queue.length === 0) { + for (let i = 0; i < gray_queue.length; i++) { + const node = gray_queue[i]; + const nodeParent = node.parent; + if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { + gray_queue.splice(i, 1); + black_queue.push(node); + setColor(node, NodeColor.Black); + i--; + } + } + } + + if (black_queue.length > 0) { + node = black_queue.shift(); + } else { + // only gray nodes remaining... + break; + } + const nodeSourceFile = node.getSourceFile(); + + const loop = (node: ts.Node) => { + const [symbol, symbolImportNode] = getRealNodeSymbol(checker, node); + if (symbolImportNode) { + setColor(symbolImportNode, NodeColor.Black); + } + + if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + for (let i = 0, len = symbol.declarations.length; i < len; i++) { + const declaration = symbol.declarations[i]; + if (ts.isSourceFile(declaration)) { + // Do not enqueue full source files + // (they can be the declaration of a module import) + continue; + } + + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + enqueue_black(declaration.name); + + for (let j = 0; j < declaration.members.length; j++) { + const member = declaration.members[j]; + const memberName = member.name ? member.name.getText() : null; + if ( + ts.isConstructorDeclaration(member) + || ts.isConstructSignatureDeclaration(member) + || ts.isIndexSignatureDeclaration(member) + || ts.isCallSignatureDeclaration(member) + || memberName === 'toJSON' + || memberName === 'toString' + || memberName === 'dispose'// TODO: keeping all `dispose` methods + ) { + enqueue_black(member); + } + } + + // queue the heritage clauses + if (declaration.heritageClauses) { + for (let heritageClause of declaration.heritageClauses) { + enqueue_black(heritageClause); + } + } + } else { + enqueue_black(declaration); + } + } + } + node.forEachChild(loop); + }; + node.forEachChild(loop); + } +} + +function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol): boolean { + for (let i = 0, len = symbol.declarations.length; i < len; i++) { + const declaration = symbol.declarations[i]; + const declarationSourceFile = declaration.getSourceFile(); + + if (nodeSourceFile === declarationSourceFile) { + if (declaration.pos <= node.pos && node.end <= declaration.end) { + return true; + } + } + } + + return false; +} + +function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult { + const program = languageService.getProgram(); + + let result: ITreeShakingResult = {}; + const writeFile = (filePath: string, contents: string): void => { + result[filePath] = contents; + }; + + program.getSourceFiles().forEach((sourceFile) => { + const fileName = sourceFile.fileName; + if (/^defaultLib:/.test(fileName)) { + return; + } + const destination = fileName; + if (/\.d\.ts$/.test(fileName)) { + if (nodeOrChildIsBlack(sourceFile)) { + writeFile(destination, sourceFile.text); + } + return; + } + + let text = sourceFile.text; + let result = ''; + + function keep(node: ts.Node): void { + result += text.substring(node.pos, node.end); + } + function write(data: string): void { + result += data; + } + + function writeMarkedNodes(node: ts.Node): void { + if (getColor(node) === NodeColor.Black) { + return keep(node); + } + + // Always keep certain top-level statements + if (ts.isSourceFile(node.parent)) { + if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') { + return keep(node); + } + + if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) { + return keep(node); + } + } + + // Keep the entire import in import * as X cases + if (ts.isImportDeclaration(node)) { + if (node.importClause && node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + if (getColor(node.importClause.namedBindings) === NodeColor.Black) { + return keep(node); + } + } else { + let survivingImports: string[] = []; + for (let i = 0; i < node.importClause.namedBindings.elements.length; i++) { + const importNode = node.importClause.namedBindings.elements[i]; + if (getColor(importNode) === NodeColor.Black) { + survivingImports.push(importNode.getFullText(sourceFile)); + } + } + const leadingTriviaWidth = node.getLeadingTriviaWidth(); + const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); + if (survivingImports.length > 0) { + if (node.importClause && getColor(node.importClause) === NodeColor.Black) { + return write(`${leadingTrivia}import ${node.importClause.name.text}, {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + return write(`${leadingTrivia}import {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } else { + if (node.importClause && getColor(node.importClause) === NodeColor.Black) { + return write(`${leadingTrivia}import ${node.importClause.name.text} from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + } + } + } else { + if (node.importClause && getColor(node.importClause) === NodeColor.Black) { + return keep(node); + } + } + } + + if (shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { + let toWrite = node.getFullText(); + for (let i = node.members.length - 1; i >= 0; i--) { + const member = node.members[i]; + if (getColor(member) === NodeColor.Black) { + // keep method + continue; + } + if (/^_(.*)Brand$/.test(member.name.getText())) { + // TODO: keep all members ending with `Brand`... + continue; + } + + let pos = member.pos - node.pos; + let end = member.end - node.pos; + toWrite = toWrite.substring(0, pos) + toWrite.substring(end); + } + return write(toWrite); + } + + if (ts.isFunctionDeclaration(node)) { + // Do not go inside functions if they haven't been marked + return; + } + + node.forEachChild(writeMarkedNodes); + } + + if (getColor(sourceFile) !== NodeColor.Black) { + if (!nodeOrChildIsBlack(sourceFile)) { + // none of the elements are reachable => don't write this file at all! + return; + } + sourceFile.forEachChild(writeMarkedNodes); + result += sourceFile.endOfFileToken.getFullText(sourceFile); + } else { + result = text; + } + + writeFile(destination, result); + }); + + return result; +} + +//#endregion + +//#region Utils + +/** + * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) + */ +function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol, ts.Declaration] { + /** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ + /* @internal */ + function getContainingObjectLiteralElement(node: ts.Node): ts.ObjectLiteralElement | undefined { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + if (node.parent.kind === ts.SyntaxKind.ComputedPropertyName) { + return ts.isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; + } + // falls through + case ts.SyntaxKind.Identifier: + return ts.isObjectLiteralElement(node.parent) && + (node.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === ts.SyntaxKind.JsxAttributes) && + node.parent.name === node ? node.parent : undefined; + } + return undefined; + } + + function getPropertySymbolsFromType(type: ts.Type, propName: ts.PropertyName) { + function getTextOfPropertyName(name: ts.PropertyName): string { + + function isStringOrNumericLiteral(node: ts.Node): node is ts.StringLiteral | ts.NumericLiteral { + const kind = node.kind; + return kind === ts.SyntaxKind.StringLiteral + || kind === ts.SyntaxKind.NumericLiteral; + } + + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return name.text; + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + return name.text; + case ts.SyntaxKind.ComputedPropertyName: + return isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined!; + } + } + + const name = getTextOfPropertyName(propName); + if (name && type) { + const result: ts.Symbol[] = []; + const symbol = type.getProperty(name); + if (type.flags & ts.TypeFlags.Union) { + for (const t of (type).types) { + const symbol = t.getProperty(name); + if (symbol) { + result.push(symbol); + } + } + return result; + } + + if (symbol) { + result.push(symbol); + return result; + } + } + return undefined; + } + + function getPropertySymbolsFromContextualType(typeChecker: ts.TypeChecker, node: ts.ObjectLiteralElement): ts.Symbol[] { + const objectLiteral = node.parent; + const contextualType = typeChecker.getContextualType(objectLiteral)!; + return getPropertySymbolsFromType(contextualType, node.name!)!; + } + + // Go to the original declaration for cases: + // + // (1) when the aliased symbol was declared in the location(parent). + // (2) when the aliased symbol is originating from an import. + // + function shouldSkipAlias(node: ts.Node, declaration: ts.Node): boolean { + if (node.kind !== ts.SyntaxKind.Identifier) { + return false; + } + if (node.parent === declaration) { + return true; + } + switch (declaration.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportEqualsDeclaration: + return true; + case ts.SyntaxKind.ImportSpecifier: + return declaration.parent.kind === ts.SyntaxKind.NamedImports; + default: + return false; + } + } + + if (!ts.isShorthandPropertyAssignment(node)) { + if (node.getChildCount() !== 0) { + return [null, null]; + } + } + + let symbol = checker.getSymbolAtLocation(node); + let importNode: ts.Declaration = null; + if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + const aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + // We should mark the import as visited + importNode = symbol.declarations[0]; + symbol = aliased; + } + } + + if (symbol) { + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + } + + // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the + // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern + // and return the property declaration for the referenced property. + // For example: + // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" + // + // function bar(onfulfilled: (value: T) => void) { //....} + // interface Test { + // pr/*destination*/op1: number + // } + // bar(({pr/*goto*/op1})=>{}); + if (ts.isPropertyName(node) && ts.isBindingElement(node.parent) && ts.isObjectBindingPattern(node.parent.parent) && + (node === (node.parent.propertyName || node.parent.name))) { + const type = checker.getTypeAtLocation(node.parent.parent); + if (type) { + const propSymbols = getPropertySymbolsFromType(type, node); + if (propSymbols) { + symbol = propSymbols[0]; + } + } + } + + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /*first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: false }) + const element = getContainingObjectLiteralElement(node); + if (element && checker.getContextualType(element.parent as ts.Expression)) { + const propertySymbols = getPropertySymbolsFromContextualType(checker, element); + if (propertySymbols) { + symbol = propertySymbols[0]; + } + } + } + + if (symbol && symbol.declarations) { + return [symbol, importNode]; + } + + return [null, null]; +} + +/** Get the token whose text contains the position */ +function getTokenAtPosition(sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node { + let current: ts.Node = sourceFile; + outer: while (true) { + // find the child that contains 'position' + for (const child of current.getChildren()) { + const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + break; + } + + const end = child.getEnd(); + if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { + current = child; + continue outer; + } + } + + return current; + } +} + +//#endregion 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/monaco/api.js b/build/monaco/api.js index 74e9273f35e..ae4a0d26616 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -134,7 +134,25 @@ function getTopLevelDeclaration(sourceFile, typeName) { function getNodeText(sourceFile, node) { return sourceFile.getFullText().substring(node.pos, node.end); } -function getMassagedTopLevelDeclarationText(sourceFile, declaration) { +function hasModifier(modifiers, kind) { + if (modifiers) { + for (var i = 0; i < modifiers.length; i++) { + var mod = modifiers[i]; + if (mod.kind === kind) { + return true; + } + } + } + return false; +} +function isStatic(member) { + return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); +} +function isDefaultExport(declaration) { + return (hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) + && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword)); +} +function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage) { var result = getNodeText(sourceFile, declaration); // if (result.indexOf('MonacoWorker') >= 0) { // console.log('here!'); @@ -142,6 +160,18 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration) { // } if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { var interfaceDeclaration = declaration; + var staticTypeName_1 = (isDefaultExport(interfaceDeclaration) + ? importName + ".default" + : importName + "." + declaration.name.text); + var instanceTypeName_1 = staticTypeName_1; + var typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); + if (typeParametersCnt > 0) { + var arr = []; + for (var i = 0; i < typeParametersCnt; i++) { + arr.push('any'); + } + instanceTypeName_1 = instanceTypeName_1 + "<" + arr.join(',') + ">"; + } var members = interfaceDeclaration.members; members.forEach(function (member) { try { @@ -151,6 +181,15 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration) { result = result.replace(memberText, ''); // console.log('AFTER: ', result); } + else { + var memberName = member.name.text; + if (isStatic(member)) { + usage.push("a = " + staticTypeName_1 + "." + memberName + ";"); + } + else { + usage.push("a = (<" + instanceTypeName_1 + ">b)." + memberName + ";"); + } + } } catch (err) { // life.. @@ -211,6 +250,16 @@ function generateDeclarationFile(out, inputFiles, recipe) { var endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; var lines = recipe.split(endl); var result = []; + var usageCounter = 0; + var usageImports = []; + var usage = []; + usage.push("var a;"); + usage.push("var b;"); + var generateUsageImport = function (moduleId) { + var importName = 'm' + (++usageCounter); + usageImports.push("import * as " + importName + " from '" + moduleId.replace(/\.d\.ts$/, '') + "';"); + return importName; + }; lines.forEach(function (line) { var m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); if (m1) { @@ -220,6 +269,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { if (!sourceFile_1) { return; } + var importName_1 = generateUsageImport(moduleId); var replacer_1 = createReplacer(m1[2]); var typeNames = m1[3].split(/,/); typeNames.forEach(function (typeName) { @@ -232,7 +282,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { logErr('Cannot find type ' + typeName); return; } - result.push(replacer_1(getMassagedTopLevelDeclarationText(sourceFile_1, declaration))); + result.push(replacer_1(getMassagedTopLevelDeclarationText(sourceFile_1, declaration, importName_1, usage))); }); return; } @@ -244,6 +294,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { if (!sourceFile_2) { return; } + var importName_2 = generateUsageImport(moduleId); var replacer_2 = createReplacer(m2[2]); var typeNames = m2[3].split(/,/); var typesToExcludeMap_1 = {}; @@ -271,7 +322,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { } } } - result.push(replacer_2(getMassagedTopLevelDeclarationText(sourceFile_2, declaration))); + result.push(replacer_2(getMassagedTopLevelDeclarationText(sourceFile_2, declaration, importName_2, usage))); }); return; } @@ -282,9 +333,12 @@ function generateDeclarationFile(out, inputFiles, recipe) { resultTxt = resultTxt.replace(/\bEvent, kind: ts.SyntaxKind): boolean { + if (modifiers) { + for (let i = 0; i < modifiers.length; i++) { + let mod = modifiers[i]; + if (mod.kind === kind) { + return true; + } + } + } + return false; +} -function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare): string { +function isStatic(member: ts.ClassElement | ts.TypeElement): boolean { + return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); +} + +function isDefaultExport(declaration: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean { + return ( + hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) + && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword) + ); +} + +function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[]): string { let result = getNodeText(sourceFile, declaration); // if (result.indexOf('MonacoWorker') >= 0) { // console.log('here!'); @@ -163,7 +185,23 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { let interfaceDeclaration = declaration; - let members: ts.NodeArray = interfaceDeclaration.members; + const staticTypeName = ( + isDefaultExport(interfaceDeclaration) + ? `${importName}.default` + : `${importName}.${declaration.name.text}` + ); + + let instanceTypeName = staticTypeName; + const typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); + if (typeParametersCnt > 0) { + let arr: string[] = []; + for (let i = 0; i < typeParametersCnt; i++) { + arr.push('any'); + } + instanceTypeName = `${instanceTypeName}<${arr.join(',')}>`; + } + + const members: ts.NodeArray = interfaceDeclaration.members; members.forEach((member) => { try { let memberText = getNodeText(sourceFile, member); @@ -171,6 +209,13 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati // console.log('BEFORE: ', result); result = result.replace(memberText, ''); // console.log('AFTER: ', result); + } else { + const memberName = (member.name).text; + if (isStatic(member)) { + usage.push(`a = ${staticTypeName}.${memberName};`); + } else { + usage.push(`a = (<${instanceTypeName}>b).${memberName};`); + } } } catch (err) { // life.. @@ -237,11 +282,24 @@ function createReplacer(data: string): (str: string) => string { }; } -function generateDeclarationFile(out: string, inputFiles: { [file: string]: string; }, recipe: string): string { +function generateDeclarationFile(out: string, inputFiles: { [file: string]: string; }, recipe: string): [string, string] { const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; let lines = recipe.split(endl); - let result = []; + let result: string[] = []; + + let usageCounter = 0; + let usageImports: string[] = []; + let usage: string[] = []; + + usage.push(`var a;`); + usage.push(`var b;`); + + const generateUsageImport = (moduleId: string) => { + let importName = 'm' + (++usageCounter); + usageImports.push(`import * as ${importName} from '${moduleId.replace(/\.d\.ts$/, '')}';`); + return importName; + }; lines.forEach(line => { @@ -254,6 +312,8 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri return; } + const importName = generateUsageImport(moduleId); + let replacer = createReplacer(m1[2]); let typeNames = m1[3].split(/,/); @@ -267,7 +327,7 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri logErr('Cannot find type ' + typeName); return; } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration))); + result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage))); }); return; } @@ -281,6 +341,8 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri return; } + const importName = generateUsageImport(moduleId); + let replacer = createReplacer(m2[2]); let typeNames = m2[3].split(/,/); @@ -309,7 +371,7 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri } } } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration))); + result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage))); }); return; } @@ -324,10 +386,13 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri resultTxt = format(resultTxt); - return resultTxt; + return [ + resultTxt, + `${usageImports.join('\n')}\n\n${usage.join('\n')}` + ]; } -export function getFilesToWatch(out: string): string[] { +function getIncludesInRecipe(): string[] { let recipe = fs.readFileSync(RECIPE_PATH).toString(); let lines = recipe.split(/\r\n|\n|\r/); let result = []; @@ -337,14 +402,14 @@ export function getFilesToWatch(out: string): string[] { let m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); if (m1) { let moduleId = m1[1]; - result.push(moduleIdToPath(out, moduleId)); + result.push(moduleId); return; } let m2 = line.match(/^\s*#includeAll\(([^;)]*)(;[^)]*)?\)\:(.*)$/); if (m2) { let moduleId = m2[1]; - result.push(moduleIdToPath(out, moduleId)); + result.push(moduleId); return; } }); @@ -352,8 +417,13 @@ export function getFilesToWatch(out: string): string[] { return result; } +export function getFilesToWatch(out: string): string[] { + return getIncludesInRecipe().map((moduleId) => moduleIdToPath(out, moduleId)); +} + export interface IMonacoDeclarationResult { content: string; + usageContent: string; filePath: string; isTheSame: boolean; } @@ -363,7 +433,7 @@ export function run(out: string, inputFiles: { [file: string]: string; }): IMona SOURCE_FILE_MAP = {}; let recipe = fs.readFileSync(RECIPE_PATH).toString(); - let result = generateDeclarationFile(out, inputFiles, recipe); + let [result, usageContent] = generateDeclarationFile(out, inputFiles, recipe); let currentContent = fs.readFileSync(DECLARATION_PATH).toString(); log('Finished monaco.d.ts generation'); @@ -374,6 +444,7 @@ export function run(out: string, inputFiles: { [file: string]: string; }): IMona return { content: result, + usageContent: usageContent, filePath: DECLARATION_PATH, isTheSame }; @@ -382,3 +453,96 @@ export function run(out: string, inputFiles: { [file: string]: string; }): IMona export function complainErrors() { logErr('Not running monaco.d.ts generation due to compile errors'); } + + + +interface ILibMap { [libName: string]: string; } +interface IFileMap { [fileName: string]: string; } + +class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + + private readonly _libs: ILibMap; + private readonly _files: IFileMap; + private readonly _compilerOptions: ts.CompilerOptions; + + constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + + // --- language service host --------------- + + getCompilationSettings(): ts.CompilerOptions { + return this._compilerOptions; + } + getScriptFileNames(): string[] { + return ( + [] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files)) + ); + } + getScriptVersion(fileName: string): string { + return '1'; + } + getProjectVersion(): string { + return '1'; + } + getScriptSnapshot(fileName: string): ts.IScriptSnapshot { + if (this._files.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._files[fileName]); + } else if (this._libs.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._libs[fileName]); + } else { + return ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(fileName: string): ts.ScriptKind { + return ts.ScriptKind.TS; + } + getCurrentDirectory(): string { + return ''; + } + getDefaultLibFileName(options: ts.CompilerOptions): string { + return 'defaultLib:es5'; + } + isDefaultLibFileName(fileName: string): boolean { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} + +export function execute(): IMonacoDeclarationResult { + + const OUTPUT_FILES: { [file: string]: string; } = {}; + const SRC_FILES: IFileMap = {}; + const SRC_FILE_TO_EXPECTED_NAME: { [filename: string]: string; } = {}; + getIncludesInRecipe().forEach((moduleId) => { + if (/\.d\.ts$/.test(moduleId)) { + let fileName = path.join(SRC, moduleId); + OUTPUT_FILES[moduleIdToPath('src', moduleId)] = fs.readFileSync(fileName).toString(); + return; + } + + let fileName = path.join(SRC, moduleId) + '.ts'; + SRC_FILES[fileName] = fs.readFileSync(fileName).toString(); + SRC_FILE_TO_EXPECTED_NAME[fileName] = moduleIdToPath('src', moduleId); + }); + + const languageService = ts.createLanguageService(new TypeScriptLanguageServiceHost({}, SRC_FILES, {})); + + var t1 = Date.now(); + Object.keys(SRC_FILES).forEach((fileName) => { + var t = Date.now(); + const emitOutput = languageService.getEmitOutput(fileName, true); + OUTPUT_FILES[SRC_FILE_TO_EXPECTED_NAME[fileName]] = emitOutput.outputFiles[0].text; + // console.log(`Generating .d.ts for ${fileName} took ${Date.now() - t} ms`); + }); + console.log(`Generating .d.ts took ${Date.now() - t1} ms`); + + // console.log(result.filePath); + // fs.writeFileSync(result.filePath, result.content.replace(/\r\n/gm, '\n')); + // fs.writeFileSync(path.join(SRC, 'user.ts'), result.usageContent.replace(/\r\n/gm, '\n')); + + return run('src', OUTPUT_FILES); +} diff --git a/build/monaco/monaco.usage.recipe b/build/monaco/monaco.usage.recipe new file mode 100644 index 00000000000..05377a19ba0 --- /dev/null +++ b/build/monaco/monaco.usage.recipe @@ -0,0 +1,82 @@ + +// This file is adding references to various symbols which should not be removed via tree shaking + +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHighlight } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { SimpleWorkerClient, create as create1 } from 'vs/base/common/worker/simpleWorker'; +import { create as create2 } from 'vs/editor/common/services/editorSimpleWorker'; +import { QuickOpenWidget } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; +import { SyncDescriptor0, SyncDescriptor1, SyncDescriptor2, SyncDescriptor3, SyncDescriptor4, SyncDescriptor5, SyncDescriptor6, SyncDescriptor7, SyncDescriptor8 } from 'vs/platform/instantiation/common/descriptors'; +import { PolyfillPromise } from 'vs/base/common/winjs.polyfill.promise'; +import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; +import * as editorAPI from 'vs/editor/editor.api'; + +(function () { + var a: any; + var b: any; + a = (b).layout; // IContextViewProvider + a = (b).getWorkspaceFolder; // IWorkspaceFolderProvider + a = (b).getWorkspace; // IWorkspaceFolderProvider + a = (b).style; // IThemable + a = (b).style; // IThemable + a = (b).userHome; // IUserHomeProvider + a = (b).previous; // IDiffNavigator + a = (>b).type; + a = (b).start; + a = (b).end; + a = (>b).getProxyObject; // IWorkerClient + a = create1; + a = create2; + + // promise polyfill + a = PolyfillPromise.all; + a = PolyfillPromise.race; + a = PolyfillPromise.resolve; + a = PolyfillPromise.reject; + a = (b).then; + a = (b).catch; + + // injection madness + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + + // exported API + a = editorAPI.CancellationTokenSource; + a = editorAPI.Emitter; + a = editorAPI.KeyCode; + a = editorAPI.KeyMod; + a = editorAPI.Position; + a = editorAPI.Range; + a = editorAPI.Selection; + a = editorAPI.SelectionDirection; + a = editorAPI.Severity; + a = editorAPI.MarkerSeverity; + a = editorAPI.MarkerTag; + a = editorAPI.Promise; + a = editorAPI.Uri; + a = editorAPI.Token; + a = editorAPI.editor; + a = editorAPI.languages; +})(); diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index 985fbd28cb7..eebf63ed330 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -43,7 +43,12 @@ function update(idOrPath) { let apiToken = process.env.TRANSIFEX_API_TOKEN; let languageId = localization.transifexId || localization.languageId; let translationDataFolder = path.join(locExtFolder, 'translations'); - + if (languageId === "zh-cn") { + languageId = "zh-hans"; + } + if (languageId === "zh-tw") { + languageId = "zh-hant"; + } if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { console.log('Clearing \'' + translationDataFolder + '\'...'); rimraf.sync(translationDataFolder); diff --git a/build/tfs/continuous-build.yml b/build/tfs/continuous-build.yml index 79d65b7de84..a2cfee54013 100644 --- a/build/tfs/continuous-build.yml +++ b/build/tfs/continuous-build.yml @@ -2,148 +2,14 @@ phases: - phase: Windows queue: Hosted VS2017 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - powershell: | - yarn - displayName: Install Dependencies - - powershell: | - yarn gulp electron - displayName: Download Electron - - powershell: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - powershell: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - powershell: | - yarn compile - displayName: Compile Sources - - powershell: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - powershell: | - .\scripts\test.bat --tfs "Unit Tests" - displayName: Run Unit Tests - - powershell: | - .\scripts\test-integration.bat --tfs "Integration Tests" - displayName: Run Integration Tests - - powershell: | - yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" - displayName: Run Smoke Tests - continueOnError: true - - task: PublishBuildArtifacts@1 - displayName: Publish Smoketest Artifacts - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' - ArtifactName: build-artifacts-win32 - publishLocation: Container - condition: eq(variables['System.PullRequest.IsFork'], 'False') - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: '*-results.xml' - searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() + - template: win32/continuous-build-win32.yml - phase: Linux queue: Hosted Linux Preview steps: - - script: | - set -e - apt-get update - apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 - cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb - chmod +x /etc/init.d/xvfb - update-rc.d xvfb defaults - ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon - service xvfb start - service dbus start - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - script: | - yarn - displayName: Install Dependencies - - script: | - yarn gulp electron-x64 - displayName: Download Electron - - script: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - script: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - script: | - yarn compile - displayName: Compile Sources - - script: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - script: | - DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" - displayName: Run Unit Tests - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: '*-results.xml' - searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() + - template: linux/continuous-build-linux.yml - phase: macOS queue: Hosted macOS Preview steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - script: | - yarn - displayName: Install Dependencies - - script: | - yarn gulp electron-x64 - displayName: Download Electron - - script: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - script: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - script: | - yarn compile - displayName: Compile Sources - - script: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - script: | - ./scripts/test.sh --tfs "Unit Tests" - displayName: Run Unit Tests - - script: | - ./scripts/test-integration.sh --tfs "Integration Tests" - displayName: Run Integration Tests - - script: | - yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" - displayName: Run Smoke Tests - continueOnError: true - - task: PublishBuildArtifacts@1 - displayName: Publish Smoketest Artifacts - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' - ArtifactName: build-artifacts-darwin - publishLocation: Container - condition: eq(variables['System.PullRequest.IsFork'], 'False') - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: '*-results.xml' - searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() \ No newline at end of file + - template: darwin/continuous-build-darwin.yml \ No newline at end of file diff --git a/build/tfs/darwin/continuous-build-darwin.yml b/build/tfs/darwin/continuous-build-darwin.yml new file mode 100644 index 00000000000..20070e08745 --- /dev/null +++ b/build/tfs/darwin/continuous-build-darwin.yml @@ -0,0 +1,48 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" +- script: | + yarn + displayName: Install Dependencies +- script: | + yarn gulp electron-x64 + displayName: Download Electron +- script: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- script: | + yarn check-monaco-editor-compilation + displayName: Run Monaco Editor Checks +- script: | + yarn compile + displayName: Compile Sources +- script: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- script: | + ./scripts/test.sh --tfs "Unit Tests" + displayName: Run Unit Tests +- script: | + ./scripts/test-integration.sh --tfs "Integration Tests" + displayName: Run Integration Tests +- script: | + yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" + displayName: Run Smoke Tests + continueOnError: true +- task: PublishBuildArtifacts@1 + displayName: Publish Smoketest Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-darwin + publishLocation: Container + condition: eq(variables['System.PullRequest.IsFork'], 'False') +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/darwin/product-build-darwin.yml b/build/tfs/darwin/product-build-darwin.yml new file mode 100644 index 00000000000..0dd31503586 --- /dev/null +++ b/build/tfs/darwin/product-build-darwin.yml @@ -0,0 +1,63 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + +- script: | + set -e + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + npm run gulp -- hygiene + npm run monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + +- script: | + set -e + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ + AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ + npm run gulp -- vscode-darwin-min upload-vscode-sourcemaps + name: build + +- script: | + set -e + ./scripts/test.sh --build --tfs "Unit Tests" + APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" + name: test + +- script: | + set -e + # archive the unsigned build + pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin-unsigned.zip * && popd + + # publish the unsigned build + PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` + VERSION=`node -p "require(\"$PACKAGEJSON\").version"` + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js \ + "$(VSCODE_QUALITY)" \ + darwin \ + archive-unsigned \ + "VSCode-darwin-$(VSCODE_QUALITY)-unsigned.zip" \ + $VERSION \ + false \ + ../VSCode-darwin-unsigned.zip + + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" + + # enqueue the unsigned build + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + node build/tfs/darwin/enqueue.js "$(VSCODE_QUALITY)" + + AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ + npm run gulp -- upload-vscode-configuration \ No newline at end of file diff --git a/build/tfs/linux/continuous-build-linux.yml b/build/tfs/linux/continuous-build-linux.yml new file mode 100644 index 00000000000..7ec3aec74b9 --- /dev/null +++ b/build/tfs/linux/continuous-build-linux.yml @@ -0,0 +1,44 @@ +steps: +- script: | + set -e + apt-get update + apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 + cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb + chmod +x /etc/init.d/xvfb + update-rc.d xvfb defaults + ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon + service xvfb start + service dbus start +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" +- script: | + yarn + displayName: Install Dependencies +- script: | + yarn gulp electron-x64 + displayName: Download Electron +- script: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- script: | + yarn check-monaco-editor-compilation + displayName: Run Monaco Editor Checks +- script: | + yarn compile + displayName: Compile Sources +- script: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- script: | + DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" + displayName: Run Unit Tests +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/linux/frozen-check.ts b/build/tfs/linux/frozen-check.ts index 489a24dc28a..c97d33b3953 100644 --- a/build/tfs/linux/frozen-check.ts +++ b/build/tfs/linux/frozen-check.ts @@ -39,4 +39,11 @@ function getConfig(quality: string): Promise { } getConfig(process.argv[2]) - .then(c => console.log(c.frozen), e => console.error(e)); \ No newline at end of file + .then(config => { + console.log(config.frozen); + process.exit(0); + }) + .catch(err => { + console.error(err); + process.exit(1); + }); \ No newline at end of file diff --git a/build/tfs/linux/product-build-linux.yml b/build/tfs/linux/product-build-linux.yml new file mode 100644 index 00000000000..f6e067ebfe3 --- /dev/null +++ b/build/tfs/linux/product-build-linux.yml @@ -0,0 +1,118 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + +- script: | + set -e + export npm_config_arch="$(VSCODE_ARCH)" + if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then + export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" + fi + + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + npm run gulp -- hygiene + npm run monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + +- script: | + set -e + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min + name: build + +- script: | + set -e + npm run gulp -- "electron-$(VSCODE_ARCH)" + DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + name: test + +- script: | + set -e + REPO="$(pwd)" + ROOT="$REPO/.." + ARCH="$(VSCODE_ARCH)" + + # Publish tarball + PLATFORM_LINUX="linux-$(VSCODE_ARCH)" + [[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" + [[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" + BUILDNAME="VSCode-$PLATFORM_LINUX" + BUILD="$ROOT/$BUILDNAME" + BUILD_VERSION="$(date +%s)" + [ -z "$VSCODE_QUALITY" ] && TARBALL_FILENAME="code-$BUILD_VERSION.tar.gz" || TARBALL_FILENAME="code-$VSCODE_QUALITY-$BUILD_VERSION.tar.gz" + TARBALL_PATH="$ROOT/$TARBALL_FILENAME" + PACKAGEJSON="$BUILD/resources/app/package.json" + VERSION=$(node -p "require(\"$PACKAGEJSON\").version") + + rm -rf $ROOT/code-*.tar.* + (cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$VERSION" true "$TARBALL_PATH" + + # Publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" + + # Publish DEB + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" + PLATFORM_DEB="linux-deb-$ARCH" + [[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" + DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" + DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_DEB" package "$DEB_FILENAME" "$VERSION" true "$DEB_PATH" + + # Publish RPM + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" + PLATFORM_RPM="linux-rpm-$ARCH" + [[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" + RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" + RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_RPM" package "$RPM_FILENAME" "$VERSION" true "$RPM_PATH" + + # SNAP_FILENAME="$(ls $REPO/.build/linux/snap/$ARCH/ | grep .snap)" + # SNAP_PATH="$REPO/.build/linux/snap/$ARCH/$SNAP_FILENAME" + + # Publish to MS repo + IS_FROZEN="$(AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" node build/tfs/linux/frozen-check.js $VSCODE_QUALITY)" + + if [ -z "$VSCODE_QUALITY" ]; then + echo "VSCODE_QUALITY is not set, skipping repo package publish" + elif [ "$IS_FROZEN" = "true" ]; then + echo "$VSCODE_QUALITY is frozen, skipping repo package publish" + else + if [ "$BUILD_SOURCEBRANCH" = "master" ] || [ "$BUILD_SOURCEBRANCH" = "refs/heads/master" ] || [ "$VSCODE_PUBLISH_LINUX" = "true" ]; then + if [[ $BUILD_QUEUEDBY = *"Project Collection Service Accounts"* || $BUILD_QUEUEDBY = *"Microsoft.VisualStudio.Services.TFS"* ]]; then + # Write config files needed by API, use eval to force environment variable expansion + pushd build/tfs/linux + # Submit to apt repo + if [ "$DEB_ARCH" = "amd64" ]; then + echo "{ \"server\": \"azure-apt-cat.cloudapp.net\", \"protocol\": \"https\", \"port\": \"443\", \"repositoryId\": \"58a4adf642421134a1a48d1a\", \"username\": \"vscode\", \"password\": \"$(LINUX_REPO_PASSWORD)\" }" > apt-config.json + ./repoapi_client.sh -config apt-config.json -addfile $DEB_PATH + fi + # Submit to yum repo (disabled as it's manual until signing is automated) + # eval echo '{ \"server\": \"azure-apt-cat.cloudapp.net\", \"protocol\": \"https\", \"port\": \"443\", \"repositoryId\": \"58a4ae3542421134a1a48d1b\", \"username\": \"vscode\", \"password\": \"$(LINUX_REPO_PASSWORD)\" }' > yum-config.json + + # ./repoapi_client.sh -config yum-config.json -addfile $RPM_PATH + popd + echo "To check repo publish status run ./repoapi_client.sh -config config.json -check " + fi + fi + fi \ No newline at end of file diff --git a/build/tfs/linux/release.sh b/build/tfs/linux/release.sh deleted file mode 100755 index ad74ce0a385..00000000000 --- a/build/tfs/linux/release.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -set -e - -# Arguments -ARCH="$1" -LINUX_REPO_PASSWORD="$2" - -# Variables -PLATFORM_LINUX="linux-$ARCH" -PLATFORM_DEB="linux-deb-$ARCH" -PLATFORM_RPM="linux-rpm-$ARCH" -[[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" -[[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" -REPO="`pwd`" -ROOT="$REPO/.." -BUILDNAME="VSCode-$PLATFORM_LINUX" -BUILD="$ROOT/$BUILDNAME" -BUILD_VERSION="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/ | sed -e 's/code-[a-z]*_//g' -e 's/\.deb$//g')" -[ -z "$VSCODE_QUALITY" ] && TARBALL_FILENAME="code-$BUILD_VERSION.tar.gz" || TARBALL_FILENAME="code-$VSCODE_QUALITY-$BUILD_VERSION.tar.gz" -TARBALL_PATH="$ROOT/$TARBALL_FILENAME" -PACKAGEJSON="$BUILD/resources/app/package.json" -VERSION=$(node -p "require(\"$PACKAGEJSON\").version") - -rm -rf $ROOT/code-*.tar.* -(cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) - -node build/tfs/common/publish.js $VSCODE_QUALITY $PLATFORM_LINUX archive-unsigned $TARBALL_FILENAME $VERSION true $TARBALL_PATH - -DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" -DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" - -node build/tfs/common/publish.js $VSCODE_QUALITY $PLATFORM_DEB package $DEB_FILENAME $VERSION true $DEB_PATH - -RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" -RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" - -node build/tfs/common/publish.js $VSCODE_QUALITY $PLATFORM_RPM package $RPM_FILENAME $VERSION true $RPM_PATH - -# SNAP_FILENAME="$(ls $REPO/.build/linux/snap/$ARCH/ | grep .snap)" -# SNAP_PATH="$REPO/.build/linux/snap/$ARCH/$SNAP_FILENAME" - -IS_FROZEN="$(node build/tfs/linux/frozen-check.js $VSCODE_QUALITY)" - -if [ -z "$VSCODE_QUALITY" ]; then - echo "VSCODE_QUALITY is not set, skipping repo package publish" -elif [ "$IS_FROZEN" = "true" ]; then - echo "$VSCODE_QUALITY is frozen, skipping repo package publish" -else - if [ "$BUILD_SOURCEBRANCH" = "master" ] || [ "$BUILD_SOURCEBRANCH" = "refs/heads/master" ]; then - if [[ $BUILD_QUEUEDBY = *"Project Collection Service Accounts"* || $BUILD_QUEUEDBY = *"Microsoft.VisualStudio.Services.TFS"* ]]; then - # Write config files needed by API, use eval to force environment variable expansion - pushd build/tfs/linux - # Submit to apt repo - if [ "$DEB_ARCH" = "amd64" ]; then - eval echo '{ \"server\": \"azure-apt-cat.cloudapp.net\", \"protocol\": \"https\", \"port\": \"443\", \"repositoryId\": \"58a4adf642421134a1a48d1a\", \"username\": \"vscode\", \"password\": \"$LINUX_REPO_PASSWORD\" }' > apt-config.json - - ./repoapi_client.sh -config apt-config.json -addfile $DEB_PATH - fi - # Submit to yum repo (disabled as it's manual until signing is automated) - # eval echo '{ \"server\": \"azure-apt-cat.cloudapp.net\", \"protocol\": \"https\", \"port\": \"443\", \"repositoryId\": \"58a4ae3542421134a1a48d1b\", \"username\": \"vscode\", \"password\": \"$LINUX_REPO_PASSWORD\" }' > yum-config.json - - # ./repoapi_client.sh -config yum-config.json -addfile $RPM_PATH - popd - echo "To check repo publish status run ./repoapi_client.sh -config config.json -check " - fi - fi -fi diff --git a/build/tfs/product-build.yml b/build/tfs/product-build.yml index e0ac588a85a..570925d6c99 100644 --- a/build/tfs/product-build.yml +++ b/build/tfs/product-build.yml @@ -1,353 +1,38 @@ phases: - phase: Windows condition: eq(variables['VSCODE_BUILD_WIN32'], 'true') - queue: - name: Hosted VS2017 - parallel: 2 - matrix: - x64: - VSCODE_ARCH: x64 - ia32: - VSCODE_ARCH: ia32 - + queue: Hosted VS2017 + variables: + VSCODE_ARCH: x64 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" + - template: win32/product-build-win32.yml - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - powershell: | - $ErrorActionPreference = "Stop" - "machine monacotools.visualstudio.com password $(VSO_PAT)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII - $env:npm_config_arch="$(VSCODE_ARCH)" - $env:CHILD_CONCURRENCY="1" - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - powershell: | - $ErrorActionPreference = "Stop" - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-min" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-copy-inno-updater" - name: build - - - powershell: | - $ErrorActionPreference = "Stop" - npm run gulp -- "electron-$(VSCODE_ARCH)" - .\scripts\test.bat --build --tfs "Unit Tests" - # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - name: test - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 - inputs: - ConnectedServiceName: 'ESRP CodeSign' - FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' - Pattern: '*.dll,*.exe,*.node' - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "keyCode": "CP-229803", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "Append", - "parameterValue": "/as" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - } - ] - SessionTimeout: 120 - - - task: NuGetCommand@2 - displayName: Install ESRPClient.exe - inputs: - restoreSolution: 'build\tfs\win32\ESRPClient\packages.config' - feedsToUse: config - nugetConfigPath: 'build\tfs\win32\ESRPClient\NuGet.config' - externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b - restoreDirectory: packages - - - task: ESRPImportCertTask@1 - displayName: Import ESRP Request Signing Certificate - inputs: - ESRP: 'ESRP CodeSign' - - - powershell: | - $ErrorActionPreference = "Stop" - .\build\tfs\win32\import-esrp-auth-cert.ps1 -AuthCertificateBase64 $(ESRP_AUTH_CERTIFICATE) -AuthCertificateKey $(ESRP_AUTH_CERTIFICATE_KEY) - displayName: Import ESRP Auth Certificate - - - powershell: | - $ErrorActionPreference = "Stop" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-archive" "vscode-win32-$(VSCODE_ARCH)-system-setup" "vscode-win32-$(VSCODE_ARCH)-user-setup" - - $Repo = "$(pwd)" - $Root = "$Repo\.." - $SystemExe = "$Repo\.build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe" - $UserExe = "$Repo\.build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe" - $Zip = "$Repo\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip" - $Build = "$Root\VSCode-win32-$(VSCODE_ARCH)" - - # get version - $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json - $Version = $PackageJson.version - $Quality = "$env:VSCODE_QUALITY" - $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(AZURE_STORAGE_ACCESS_KEY_2)" - $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" - $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" - - $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } - - node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip - node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $SystemExe - node build/tfs/common/publish.js $Quality "$global:assetPlatform-user" setup "VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $UserExe - - # publish hockeyapp symbols - $hockeyAppId = if ("$(VSCODE_ARCH)" -eq "ia32") { "$(VSCODE_HOCKEYAPP_ID_WIN32)" } else { "$(VSCODE_HOCKEYAPP_ID_WIN64)" } - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId +- phase: Windows32 + condition: eq(variables['VSCODE_BUILD_WIN32_32BIT'], 'true') + queue: Hosted VS2017 + variables: + VSCODE_ARCH: ia32 + steps: + - template: win32/product-build-win32.yml - phase: Linux condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') queue: linux-x64 variables: VSCODE_ARCH: x64 - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - export npm_config_arch="$(VSCODE_ARCH)" - if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then - export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" - fi - - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build - - - script: | - set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" - DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test - - - script: | - set -e - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - ./build/tfs/linux/release.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" + - template: linux/product-build-linux.yml - phase: Linux32 - condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') + condition: eq(variables['VSCODE_BUILD_LINUX_32BIT'], 'true') queue: linux-ia32 variables: VSCODE_ARCH: ia32 - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - export npm_config_arch="$(VSCODE_ARCH)" - if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then - export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" - fi - - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build - - - script: | - set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" - DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test - - - script: | - set -e - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - ./build/tfs/linux/release.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX32)" + - template: linux/product-build-linux.yml - phase: macOS condition: eq(variables['VSCODE_BUILD_MACOS'], 'true') queue: Hosted macOS Preview steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - npm run gulp -- vscode-darwin-min upload-vscode-sourcemaps - name: build - - - script: | - set -e - ./scripts/test.sh --build --tfs "Unit Tests" - APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" - name: test - - - script: | - set -e - # archive the unsigned build - pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin-unsigned.zip * && popd - - # publish the unsigned build - PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` - VERSION=`node -p "require(\"$PACKAGEJSON\").version"` - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - node build/tfs/common/publish.js \ - "$(VSCODE_QUALITY)" \ - darwin \ - archive-unsigned \ - "VSCode-darwin-$(VSCODE_QUALITY)-unsigned.zip" \ - $VERSION \ - false \ - ../VSCode-darwin-unsigned.zip - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" - - # enqueue the unsigned build - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/tfs/darwin/enqueue.js "$(VSCODE_QUALITY)" - - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - npm run gulp -- upload-vscode-configuration \ No newline at end of file + - template: darwin/product-build-darwin.yml \ No newline at end of file diff --git a/build/tfs/win32/continuous-build-win32.yml b/build/tfs/win32/continuous-build-win32.yml new file mode 100644 index 00000000000..6490c637b78 --- /dev/null +++ b/build/tfs/win32/continuous-build-win32.yml @@ -0,0 +1,66 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn } + displayName: Install Dependencies +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn gulp electron } + displayName: Download Electron +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn gulp hygiene } + displayName: Run Hygiene Checks +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn check-monaco-editor-compilation } + displayName: Run Monaco Editor Checks +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn compile } + displayName: Compile Sources +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn download-builtin-extensions } + displayName: Download Built-in Extensions +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test.bat --tfs "Unit Tests" } + displayName: Run Unit Tests +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test-integration.bat --tfs "Integration Tests" } + displayName: Run Integration Tests +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" } + displayName: Run Smoke Tests + continueOnError: true +- task: PublishBuildArtifacts@1 + displayName: Publish Smoketest Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-win32 + publishLocation: Container + condition: eq(variables['System.PullRequest.IsFork'], 'False') +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() diff --git a/build/tfs/win32/exec.ps1 b/build/tfs/win32/exec.ps1 new file mode 100644 index 00000000000..826cefdf7dd --- /dev/null +++ b/build/tfs/win32/exec.ps1 @@ -0,0 +1,24 @@ +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. + +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} \ No newline at end of file diff --git a/build/tfs/win32/product-build-win32.yml b/build/tfs/win32/product-build-win32.yml new file mode 100644 index 00000000000..c887ade31fd --- /dev/null +++ b/build/tfs/win32/product-build-win32.yml @@ -0,0 +1,166 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.3.2" + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + "machine monacotools.visualstudio.com password $(VSO_PAT)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII + $env:npm_config_arch="$(VSCODE_ARCH)" + $env:CHILD_CONCURRENCY="1" + $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" + exec { yarn } + exec { npm run gulp -- hygiene } + exec { npm run monaco-compile-check } + exec { npm run gulp -- mixin } + exec { node build/tfs/common/installDistro.js } + exec { node build/lib/builtInExtensions.js } + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" + exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-min" } + exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-copy-inno-updater" } + name: build + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp -- "electron-$(VSCODE_ARCH)" } + exec { .\scripts\test.bat --build --tfs "Unit Tests" } + # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + name: test + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' + Pattern: '*.dll,*.exe,*.node' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-229803", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "Append", + "parameterValue": "/as" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + +- task: NuGetCommand@2 + displayName: Install ESRPClient.exe + inputs: + restoreSolution: 'build\tfs\win32\ESRPClient\packages.config' + feedsToUse: config + nugetConfigPath: 'build\tfs\win32\ESRPClient\NuGet.config' + externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b + restoreDirectory: packages + +- task: ESRPImportCertTask@1 + displayName: Import ESRP Request Signing Certificate + inputs: + ESRP: 'ESRP CodeSign' + +- powershell: | + $ErrorActionPreference = "Stop" + .\build\tfs\win32\import-esrp-auth-cert.ps1 -AuthCertificateBase64 $(ESRP_AUTH_CERTIFICATE) -AuthCertificateKey $(ESRP_AUTH_CERTIFICATE_KEY) + displayName: Import ESRP Auth Certificate + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-archive" "vscode-win32-$(VSCODE_ARCH)-system-setup" "vscode-win32-$(VSCODE_ARCH)-user-setup" } + + $Repo = "$(pwd)" + $Root = "$Repo\.." + $SystemExe = "$Repo\.build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe" + $UserExe = "$Repo\.build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe" + $Zip = "$Repo\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip" + $Build = "$Root\VSCode-win32-$(VSCODE_ARCH)" + + # get version + $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json + $Version = $PackageJson.version + $Quality = "$env:VSCODE_QUALITY" + $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(AZURE_STORAGE_ACCESS_KEY_2)" + $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" + + $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } + + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip } + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $SystemExe } + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform-user" setup "VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $UserExe } + + # publish hockeyapp symbols + $hockeyAppId = if ("$(VSCODE_ARCH)" -eq "ia32") { "$(VSCODE_HOCKEYAPP_ID_WIN32)" } else { "$(VSCODE_HOCKEYAPP_ID_WIN64)" } + exec { node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId } 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/extensions/configuration-editing/package.nls.json b/extensions/configuration-editing/package.nls.json index b8c247a9de3..20a9c1af8b7 100644 --- a/extensions/configuration-editing/package.nls.json +++ b/extensions/configuration-editing/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Configuration Editing", - "description": "Provides capabilities (advanced IntelliSense, auto-fixing) in configuration files like settings, launch and extension recommendation files." + "description": "Provides capabilities (advanced IntelliSense, auto-fixing) in configuration files like settings, launch, and extension recommendation files." } \ No newline at end of file diff --git a/extensions/css-language-features/CONTRIBUTING.md b/extensions/css-language-features/CONTRIBUTING.md new file mode 100644 index 00000000000..be3c614d9b7 --- /dev/null +++ b/extensions/css-language-features/CONTRIBUTING.md @@ -0,0 +1,38 @@ + +## Setup + +- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) +- Run `yarn` at `/`, this will install + - Dependencies for `/extension/css-language-features/` + - Dependencies for `/extension/css-language-features/server/` + - devDependencies such as `gulp` +- Open `/extensions/css-language-features/` as the workspace in VS Code +- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/css-language-features/.vscode/launch.json) debug target in the Debug View. This will: + - Launch the `preLaunchTask` task to compile the extension + - Launch a new VS Code instance with the `css-language-features` extension loaded + - You should see a notification saying the development version of `css-language-features` overwrites the bundled version of `css-language-features` +- Test the behavior of this extension by editing CSS/SCSS/Less files +- Run `Reload Window` command in the launched instance to reload the extension + +### Contribute to vscode-css-languageservice + +[Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) contains the language smarts for CSS/SCSS/Less. +This extension wraps the css language service into a Language Server for VS Code. +If you want to fix CSS/SCSS/Less issues or make improvements, you should make changes at [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice). + +However, within this extension, you can run a development version of `vscode-css-languageservice` to debug code or test language features interactively: + +#### Linking `vscode-css-languageservice` in `css-language-features/server/` + +- Clone [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) +- Run `yarn` in `vscode-css-languageservice` +- Run `yarn link` in `vscode-css-languageservice`. This will compile and link `vscode-css-languageservice` +- In `css-language-features/server/`, run `npm link vscode-css-languageservice` + +#### Testing the development version of `vscode-css-languageservice` + +- Open both `vscode-css-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature +- Run `yarn watch` at `css-languagefeatures/server/` to recompile this extension with the linked version of `vscode-css-languageservice` +- Make some changes in `vscode-css-languageservice` +- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-css-languageservice`. You can interactively test the language features. +- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-css-languageservice` and `css-language-features/server/` \ No newline at end of file diff --git a/extensions/css-language-features/README.md b/extensions/css-language-features/README.md index 4c72839bdc8..5a3fad4948b 100644 --- a/extensions/css-language-features/README.md +++ b/extensions/css-language-features/README.md @@ -1,39 +1,9 @@ -# CSS Language Features +# Language Features for CSS, SCSS, and LESS files -This extension offers CSS/SCSS/Less support in VS Code. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. -## Development +## Features -- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) -- Run `yarn` at `/`, this will install - - Dependencies for `/extension/css-language-features/` - - Dependencies for `/extension/css-language-features/server/` - - devDependencies such as `gulp` -- Open `/extensions/css-language-features/` as the workspace in VS Code -- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/css-language-features/.vscode/launch.json) debug target in the Debug View. This will: - - Launch the `preLaunchTask` task to compile the extension - - Launch a new VS Code instance with the `css-language-features` extension loaded - - You should see a notification saying the development version of `css-language-features` overwrites the bundled version of `css-language-features` -- Test the behavior of this extension by editing CSS/SCSS/Less files -- Run `Reload Window` command in the launched instance to reload the extension +See [CSS, SCSS and Less in VS Code](https://code.visualstudio.com/docs/languages/css) to learn about the features of this extension. -### Contribute to vscode-css-languageservice - -[Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) contains the language smarts for CSS/SCSS/Less, and this extension wraps the service into a Language Server for VS Code. If you want to fix CSS/SCSS/Less issues or make improvements, you should make changes at [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice). - -However, within this extension, you can run a development version of `vscode-css-languageservice` to debug code or test language features interactively: - -#### Linking `vscode-css-languageservice` in `css-language-features/server/` - -- Clone [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) -- Run `yarn` in `vscode-css-languageservice` -- Run `yarn link` in `vscode-css-languageservice`. This will compile and link `vscode-css-languageservice` -- In `css-language-features/server/`, run `npm link vscode-css-languageservice` - -#### Testing the development version of `vscode-css-languageservice` - -- Open both `vscode-css-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature -- Run `yarn watch` at `css-languagefeatures/server/` to recompile this extension with the linked version of `vscode-css-languageservice` -- Make some changes in `vscode-css-languageservice` -- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-css-languageservice`. You can interactively test the language features. -- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-css-languageservice` and `css-language-features/server/` +Please read the [CONTRIBUTING.md](https://github.com/Microsoft/vscode/blob/master/extensions/css-language-features/CONTRIBUTING.md) file to learn how to contribute to this extension. \ No newline at end of file diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index ec7659719bb..f426e662f58 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.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/octref/language-css/commit/ea1d7e3619966e47c57498913a5eabea0cce7538", + "version": "https://github.com/octref/language-css/commit/f8a70d3f2540a7bf12a7cbc13c1d7bb76cc8cdd0", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -606,8 +606,8 @@ ] }, { - "begin": "(?i)(?=@[\\w-]+(\\s|\\(|/\\*|$))", - "end": "(?<=})(?!\\G)", + "begin": "(?i)(?=@[\\w-]+(\\s|\\(|{|;|/\\*|$))", + "end": "(?<=}|;)(?!\\G)", "patterns": [ { "begin": "(?i)\\G(@)[\\w-]+", @@ -1397,7 +1397,7 @@ "property-names": { "patterns": [ { - "match": "(?xi) (? { + const cmdName = basename(cmdPath, '.exe'); + if (cmdName === 'node') { + const name = localize('process.with.pid.label', "Process {0}", pid); + attachToProcess(undefined, name, pid, args); + } + }); +} diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts new file mode 100644 index 00000000000..e10846e111e --- /dev/null +++ b/extensions/debug-auto-launch/src/extension.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { basename } from 'path'; +import { pollProcesses, attachToProcess } from './nodeProcessTree'; + +const localize = nls.loadMessageBundle(); +const ON_TEXT = localize('status.text.auto.attach.on', "Auto Attach: On"); +const OFF_TEXT = localize('status.text.auto.attach.off', "Auto Attach: Off"); + +const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach'; + +let currentState: string; +let autoAttacher: vscode.Disposable | undefined; +let statusItem: vscode.StatusBarItem | undefined = undefined; + + +export function activate(context: vscode.ExtensionContext): void { + + context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttach)); + + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.node.autoAttach')) { + updateAutoAttachInStatus(context); + } + })); + + updateAutoAttachInStatus(context); +} + +export function deactivate(): void { +} + + +function toggleAutoAttach(context: vscode.ExtensionContext) { + + const conf = vscode.workspace.getConfiguration('debug.node'); + + let value = conf.get('autoAttach'); + if (value === 'on') { + value = 'off'; + } else { + value = 'on'; + } + + const info = conf.inspect('autoAttach'); + let target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global; + if (info) { + if (info.workspaceFolderValue) { + target = vscode.ConfigurationTarget.WorkspaceFolder; + } else if (info.workspaceValue) { + target = vscode.ConfigurationTarget.Workspace; + } else if (info.globalValue) { + target = vscode.ConfigurationTarget.Global; + } else if (info.defaultValue) { + // setting not yet used: store setting in workspace + if (vscode.workspace.workspaceFolders) { + target = vscode.ConfigurationTarget.Workspace; + } + } + } + conf.update('autoAttach', value, target); + + updateAutoAttachInStatus(context); +} + +function updateAutoAttachInStatus(context: vscode.ExtensionContext) { + + const newState = vscode.workspace.getConfiguration('debug.node').get('autoAttach'); + + if (newState !== currentState) { + + currentState = newState; + + if (newState === 'disabled') { + + // turn everything off + if (statusItem) { + statusItem.hide(); + statusItem.text = OFF_TEXT; + } + if (autoAttacher) { + autoAttacher.dispose(); + autoAttacher = undefined; + } + + } else { // 'on' or 'off' + + // make sure status bar item exists and is visible + if (!statusItem) { + statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + statusItem.command = TOGGLE_COMMAND; + statusItem.text = OFF_TEXT; + statusItem.tooltip = localize('status.tooltip.auto.attach', "Automatically attach to node.js processes in debug mode"); + statusItem.show(); + context.subscriptions.push(statusItem); + } else { + statusItem.show(); + } + + if (newState === 'off') { + statusItem.text = OFF_TEXT; + if (autoAttacher) { + autoAttacher.dispose(); + autoAttacher = undefined; + } + } else if (newState === 'on') { + statusItem.text = ON_TEXT; + const vscode_pid = process.env['VSCODE_PID']; + const rootPid = vscode_pid ? parseInt(vscode_pid) : 0; + autoAttacher = startAutoAttach(rootPid); + } + } + } +} + +function startAutoAttach(rootPid: number): vscode.Disposable { + + return pollProcesses(rootPid, true, (pid, cmdPath, args) => { + const cmdName = basename(cmdPath, '.exe'); + if (cmdName === 'node') { + const name = localize('process.with.pid.label', "Process {0}", pid); + attachToProcess(undefined, name, pid, args); + } + }); +} diff --git a/extensions/debug-auto-launch/src/nodeProcessTree.ts b/extensions/debug-auto-launch/src/nodeProcessTree.ts new file mode 100644 index 00000000000..16bc55476e1 --- /dev/null +++ b/extensions/debug-auto-launch/src/nodeProcessTree.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { getProcessTree, ProcessTreeNode } from './processTree'; +import { analyseArguments } from './protocolDetection'; + +const pids = new Set(); + +const POLL_INTERVAL = 1000; + +/** + * Poll for all subprocesses of given root process. + */ +export function pollProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void): vscode.Disposable { + + let stopped = false; + + function poll() { + //const start = Date.now(); + findChildProcesses(rootPid, inTerminal, cb).then(_ => { + //console.log(`duration: ${Date.now() - start}`); + setTimeout(_ => { + if (!stopped) { + poll(); + } + }, POLL_INTERVAL); + }); + } + + poll(); + + return new vscode.Disposable(() => stopped = true); +} + +export function attachToProcess(folder: vscode.WorkspaceFolder | undefined, name: string, pid: number, args: string, baseConfig?: vscode.DebugConfiguration) { + + if (pids.has(pid)) { + return; + } + pids.add(pid); + + const config: vscode.DebugConfiguration = { + type: 'node', + request: 'attach', + name: name, + stopOnEntry: false + }; + + if (baseConfig) { + // selectively copy attributes + if (baseConfig.timeout) { + config.timeout = baseConfig.timeout; + } + if (baseConfig.sourceMaps) { + config.sourceMaps = baseConfig.sourceMaps; + } + if (baseConfig.outFiles) { + config.outFiles = baseConfig.outFiles; + } + if (baseConfig.sourceMapPathOverrides) { + config.sourceMapPathOverrides = baseConfig.sourceMapPathOverrides; + } + if (baseConfig.smartStep) { + config.smartStep = baseConfig.smartStep; + } + if (baseConfig.skipFiles) { + config.skipFiles = baseConfig.skipFiles; + } + if (baseConfig.showAsyncStacks) { + config.sourceMaps = baseConfig.showAsyncStacks; + } + if (baseConfig.trace) { + config.trace = baseConfig.trace; + } + } + + let { usePort, protocol, port } = analyseArguments(args); + if (usePort) { + config.processId = `${protocol}${port}`; + } else { + if (protocol && port > 0) { + config.processId = `${pid}${protocol}${port}`; + } else { + config.processId = pid.toString(); + } + } + + vscode.debug.startDebugging(folder, config); +} + +function findChildProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void): Promise { + + function walker(node: ProcessTreeNode, terminal: boolean, renderer: number) { + + if (node.args.indexOf('--type=terminal') >= 0 && (renderer === 0 || node.ppid === renderer)) { + terminal = true; + } + + let { protocol } = analyseArguments(node.args); + if (terminal && protocol) { + cb(node.pid, node.command, node.args); + } + + for (const child of node.children || []) { + walker(child, terminal, renderer); + } + } + + function finder(node: ProcessTreeNode, pid: number): ProcessTreeNode | undefined { + if (node.pid === pid) { + return node; + } + for (const child of node.children || []) { + const p = finder(child, pid); + if (p) { + return p; + } + } + return undefined; + } + + return getProcessTree(rootPid).then(tree => { + if (tree) { + + // find the pid of the renderer process + const extensionHost = finder(tree, process.pid); + let rendererPid = extensionHost ? extensionHost.ppid : 0; + + for (const child of tree.children || []) { + walker(child, !inTerminal, rendererPid); + } + } + }); +} diff --git a/extensions/debug-auto-launch/src/processTree.ts b/extensions/debug-auto-launch/src/processTree.ts new file mode 100644 index 00000000000..f072d9cf58c --- /dev/null +++ b/extensions/debug-auto-launch/src/processTree.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { spawn, ChildProcess } from 'child_process'; +import { join } from 'path'; + +export class ProcessTreeNode { + children?: ProcessTreeNode[]; + + constructor(public pid: number, public ppid: number, public command: string, public args: string) { + } +} + +export async function getProcessTree(rootPid: number): Promise { + + const map = new Map(); + + map.set(0, new ProcessTreeNode(0, 0, '???', '')); + + try { + await getProcesses((pid: number, ppid: number, command: string, args: string) => { + if (pid !== ppid) { + map.set(pid, new ProcessTreeNode(pid, ppid, command, args)); + } + }); + } catch (err) { + return undefined; + } + + const values = map.values(); + for (const p of values) { + const parent = map.get(p.ppid); + if (parent && parent !== p) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(p); + } + } + + if (!isNaN(rootPid) && rootPid > 0) { + return map.get(rootPid); + } + return map.get(0); +} + +export function getProcesses(one: (pid: number, ppid: number, command: string, args: string, date?: number) => void): Promise { + + // returns a function that aggregates chunks of data until one or more complete lines are received and passes them to a callback. + function lines(callback: (a: string) => void) { + let unfinished = ''; // unfinished last line of chunk + return (data: string | Buffer) => { + const lines = data.toString().split(/\r?\n/); + const finishedLines = lines.slice(0, lines.length - 1); + finishedLines[0] = unfinished + finishedLines[0]; // complete previous unfinished line + unfinished = lines[lines.length - 1]; // remember unfinished last line of this chunk for next round + for (const s of finishedLines) { + callback(s); + } + }; + } + + return new Promise((resolve, reject) => { + + let proc: ChildProcess; + + if (process.platform === 'win32') { + + // attributes columns are in alphabetic order! + const CMD_PAT = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)\s+([0-9]+)$/; + + const wmic = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'wbem', 'WMIC.exe'); + proc = spawn(wmic, ['process', 'get', 'CommandLine,CreationDate,ParentProcessId,ProcessId']); + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', lines(line => { + let matches = CMD_PAT.exec(line.trim()); + if (matches && matches.length === 5) { + const pid = Number(matches[4]); + const ppid = Number(matches[3]); + const date = Number(matches[2]); + let args = matches[1].trim(); + if (!isNaN(pid) && !isNaN(ppid) && args) { + let command = args; + if (args[0] === '"') { + const end = args.indexOf('"', 1); + if (end > 0) { + command = args.substr(1, end - 1); + args = args.substr(end + 2); + } + } else { + const end = args.indexOf(' '); + if (end > 0) { + command = args.substr(0, end); + args = args.substr(end + 1); + } else { + args = ''; + } + } + one(pid, ppid, command, args, date); + } + } + })); + + } else if (process.platform === 'darwin') { // OS X + + proc = spawn('/bin/ps', ['-x', '-o', `pid,ppid,comm=${'a'.repeat(256)},command`]); + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', lines(line => { + + const pid = Number(line.substr(0, 5)); + const ppid = Number(line.substr(6, 5)); + const command = line.substr(12, 256).trim(); + const args = line.substr(269 + command.length); + + if (!isNaN(pid) && !isNaN(ppid)) { + one(pid, ppid, command, args); + } + })); + + } else { // linux + + proc = spawn('/bin/ps', ['-ax', '-o', 'pid,ppid,comm:20,command']); + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', lines(line => { + + const pid = Number(line.substr(0, 5)); + const ppid = Number(line.substr(6, 5)); + let command = line.substr(12, 20).trim(); + let args = line.substr(33); + + let pos = args.indexOf(command); + if (pos >= 0) { + pos = pos + command.length; + while (pos < args.length) { + if (args[pos] === ' ') { + break; + } + pos++; + } + command = args.substr(0, pos); + args = args.substr(pos + 1); + } + + if (!isNaN(pid) && !isNaN(ppid)) { + one(pid, ppid, command, args); + } + })); + } + + proc.on('error', err => { + reject(err); + }); + + proc.stderr.setEncoding('utf8'); + proc.stderr.on('data', data => { + reject(new Error(data.toString())); + }); + + proc.on('close', (code, signal) => { + if (code === 0) { + resolve(); + } else if (code > 0) { + reject(new Error(`process terminated with exit code: ${code}`)); + } + if (signal) { + reject(new Error(`process terminated with signal: ${signal}`)); + } + }); + + proc.on('exit', (code, signal) => { + if (code === 0) { + //resolve(); + } else if (code > 0) { + reject(new Error(`process terminated with exit code: ${code}`)); + } + if (signal) { + reject(new Error(`process terminated with signal: ${signal}`)); + } + }); + }); +} + diff --git a/extensions/debug-auto-launch/src/protocolDetection.ts b/extensions/debug-auto-launch/src/protocolDetection.ts new file mode 100644 index 00000000000..9ccca40e849 --- /dev/null +++ b/extensions/debug-auto-launch/src/protocolDetection.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export const INSPECTOR_PORT_DEFAULT = 9229; +export const LEGACY_PORT_DEFAULT = 5858; + +export interface DebugArguments { + usePort: boolean; // if true debug by using the debug port + protocol?: 'legacy' | 'inspector'; + address?: string; + port: number; +} + +/* + * analyse the given command line arguments and extract debug port and protocol from it. + */ +export function analyseArguments(args: string): DebugArguments { + + const DEBUG_FLAGS_PATTERN = /--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/; + const DEBUG_PORT_PATTERN = /--(inspect|debug)-port=(\d+)/; + + const result: DebugArguments = { + usePort: false, + port: -1 + }; + + // match --debug, --debug=1234, --debug-brk, debug-brk=1234, --inspect, --inspect=1234, --inspect-brk, --inspect-brk=1234 + let matches = DEBUG_FLAGS_PATTERN.exec(args); + if (matches && matches.length >= 2) { + // attach via port + result.usePort = true; + if (matches.length >= 6 && matches[5]) { + result.address = matches[5]; + } + if (matches.length >= 7 && matches[6]) { + result.port = parseInt(matches[6]); + } + result.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector'; + } + + // a debug-port=1234 or --inspect-port=1234 overrides the port + matches = DEBUG_PORT_PATTERN.exec(args); + if (matches && matches.length === 3) { + // override port + result.port = parseInt(matches[2]); + result.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector'; + } + + if (result.port < 0) { + result.port = result.protocol === 'inspector' ? INSPECTOR_PORT_DEFAULT : LEGACY_PORT_DEFAULT; + } + + return result; +} diff --git a/extensions/debug-auto-launch/src/typings/ref.d.ts b/extensions/debug-auto-launch/src/typings/ref.d.ts new file mode 100644 index 00000000000..bc057c55878 --- /dev/null +++ b/extensions/debug-auto-launch/src/typings/ref.d.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// diff --git a/extensions/debug-auto-launch/tsconfig.json b/extensions/debug-auto-launch/tsconfig.json new file mode 100644 index 00000000000..2a5517b5543 --- /dev/null +++ b/extensions/debug-auto-launch/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "outDir": "./out", + "lib": [ + "es2015" + ], + "strict": true, + "noUnusedLocals": true, + "downlevelIteration": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock new file mode 100644 index 00000000000..5b1dccc447c --- /dev/null +++ b/extensions/debug-auto-launch/yarn.lock @@ -0,0 +1,11 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@8.0.33": + version "8.0.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" + +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/emmet/CONTRIBUTING.md b/extensions/emmet/CONTRIBUTING.md new file mode 100644 index 00000000000..c6c334828c3 --- /dev/null +++ b/extensions/emmet/CONTRIBUTING.md @@ -0,0 +1,14 @@ +## How to build and run from source? + +Read the basics about extension authoring from [Extending Visual Studio Code](https://code.visualstudio.com/docs/extensions/overview) + +- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code +- Open the `extensions/emmet` folder in the vscode repo in VS Code +- Press F5 to start debugging + +## Running tests + +Tests for Emmet extension are run as integration tests as part of VS Code. + +- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code +- Run `./scripts/test-integration.sh` to run all the integrations tests that include the Emmet tests. \ No newline at end of file diff --git a/extensions/emmet/README.md b/extensions/emmet/README.md index a2720bbd541..b755345f787 100644 --- a/extensions/emmet/README.md +++ b/extensions/emmet/README.md @@ -1,25 +1,9 @@ # Emmet integration in Visual Studio Code +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + ## Features -See [Emmet in Visual Studio Code](https://code.visualstudio.com/docs/editor/emmet) - -## How to build and run from source? - -Read the basics about extension authoring from [Extending Visual Studio Code](https://code.visualstudio.com/docs/extensions/overview) - -- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code -- Open the `extensions/emmet` folder in the vscode repo in VS Code -- Press F5 to start debugging - -## Running tests - -Tests for Emmet extension are run as integration tests as part of VS Code. - -- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code -- Run `./scripts/test-integration.sh` to run all the integrations tests that include the Emmet tests. - - - - +See [Emmet in Visual Studio Code](https://code.visualstudio.com/docs/editor/emmet) to learn about the features of this extension. +Please read the [CONTRIBUTING.md](https://github.com/Microsoft/vscode/blob/master/extensions/emmet/CONTRIBUTING.md) file to learn how to contribute to this extension. \ No newline at end of file diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 0e486252598..04b0521c591 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as path from 'path'; import * as nls from 'vscode-nls'; @@ -265,12 +264,12 @@ export class ExtensionLinter { private async loadPackageJson(folder: Uri) { const file = folder.with({ path: path.posix.join(folder.path, 'package.json') }); - const exists = await fileExists(file.fsPath); - if (!exists) { + try { + const document = await workspace.openTextDocument(file); + return parseTree(document.getText()); + } catch (err) { return undefined; } - const document = await workspace.openTextDocument(file); - return parseTree(document.getText()); } private packageJsonChanged(folder: Uri) { @@ -338,20 +337,6 @@ function endsWith(haystack: string, needle: string): boolean { } } -function fileExists(path: string): Promise { - return new Promise((resolve, reject) => { - fs.lstat(path, (err, stats) => { - if (!err) { - resolve(true); - } else if (err.code === 'ENOENT') { - resolve(false); - } else { - reject(err); - } - }); - }); -} - function parseUri(src: string) { try { return Uri.parse(src); diff --git a/extensions/git/README.md b/extensions/git/README.md index b97ec81e9a0..bd7a8ef26c2 100644 --- a/extensions/git/README.md +++ b/extensions/git/README.md @@ -1,2 +1,7 @@ # Git integration for Visual Studio Code +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +See [Git support in VS Code](https://code.visualstudio.com/docs/editor/versioncontrol#_git-support) to learn about the features of this extension. \ No newline at end of file diff --git a/extensions/git/package.json b/extensions/git/package.json index 82c3988c713..5eb0b3aab4c 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%" } } }, @@ -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/api.ts b/extensions/git/src/api.ts index 6832fda9531..cfce4c9ed8b 100644 --- a/extensions/git/src/api.ts +++ b/extensions/git/src/api.ts @@ -42,19 +42,24 @@ export interface API { export class APIImpl implements API { - constructor(private modelPromise: Promise) { } + constructor(private model: Model) { } async getGitPath(): Promise { - const model = await this.modelPromise; - return model.git.path; + return this.model.git.path; } async getRepositories(): Promise { - const model = await this.modelPromise; - return model.repositories.map(repository => new RepositoryImpl(repository)); + return this.model.repositories.map(repository => new RepositoryImpl(repository)); } } -export function createApi(modelPromise: Promise): API { - return new APIImpl(modelPromise); -} \ No newline at end of file +export class NoopAPIImpl implements API { + + async getGitPath(): Promise { + throw new Error('Git model not found'); + } + + async getRepositories(): Promise { + throw new Error('Git model not found'); + } +} 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..a360264a394 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 { @@ -344,7 +345,7 @@ function getGitErrorCode(stderr: string): string | undefined { return GitErrorCodes.RepositoryIsLocked; } else if (/Authentication failed/.test(stderr)) { return GitErrorCodes.AuthenticationFailed; - } else if (/Not a git repository/.test(stderr)) { + } else if (/Not a git repository/i.test(stderr)) { return GitErrorCodes.NotAGitRepository; } else if (/bad config file/.test(stderr)) { return GitErrorCodes.BadConfigFile; @@ -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/main.ts b/extensions/git/src/main.ts index fa155966bbf..6eae1da2c9d 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -16,12 +16,18 @@ import { GitDecorations } from './decorationProvider'; import { Askpass } from './askpass'; import { toDisposable, filterEvent, eventToPromise } from './util'; import TelemetryReporter from 'vscode-extension-telemetry'; -import { API, createApi } from './api'; +import { API, NoopAPIImpl, APIImpl } from './api'; import { GitProtocolHandler } from './protocolHandler'; -let telemetryReporter: TelemetryReporter; +const deactivateTasks: { (): Promise; }[] = []; -async function init(context: ExtensionContext, outputChannel: OutputChannel, disposables: Disposable[]): Promise { +export async function deactivate(): Promise { + for (const task of deactivateTasks) { + await task(); + } +} + +async function createModel(context: ExtensionContext, outputChannel: OutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise { const pathHint = workspace.getConfiguration('git').get('path'); const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path))); const askpass = new Askpass(); @@ -63,13 +69,30 @@ async function init(context: ExtensionContext, outputChannel: OutputChannel, dis return model; } -async function _activate(context: ExtensionContext, disposables: Disposable[]): Promise { +export async function activate(context: ExtensionContext): Promise { + const disposables: Disposable[] = []; + context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); + const outputChannel = window.createOutputChannel('Git'); commands.registerCommand('git.showOutput', () => outputChannel.show()); disposables.push(outputChannel); + const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string }; + const telemetryReporter = new TelemetryReporter(name, version, aiKey); + deactivateTasks.push(() => telemetryReporter.dispose()); + + const config = workspace.getConfiguration('git', null); + const enabled = config.get('enabled'); + + if (!enabled) { + const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git')); + const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get('enabled') === true); + await eventToPromise(onEnabled); + } + try { - return await init(context, outputChannel, disposables); + const model = await createModel(context, outputChannel, telemetryReporter, disposables); + return new APIImpl(model); } catch (err) { if (!/Git installation not found/.test(err.message || '')) { throw err; @@ -78,60 +101,30 @@ async function _activate(context: ExtensionContext, disposables: Disposable[]): const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreMissingGitWarning') === true; - if (shouldIgnore) { - return; + if (!shouldIgnore) { + console.warn(err.message); + outputChannel.appendLine(err.message); + outputChannel.show(); + + const download = localize('downloadgit', "Download Git"); + const neverShowAgain = localize('neverShowAgain', "Don't Show Again"); + const choice = await window.showWarningMessage( + localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."), + download, + neverShowAgain + ); + + if (choice === download) { + commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); + } else if (choice === neverShowAgain) { + await config.update('ignoreMissingGitWarning', true, true); + } } - console.warn(err.message); - outputChannel.appendLine(err.message); - outputChannel.show(); - - const download = localize('downloadgit', "Download Git"); - const neverShowAgain = localize('neverShowAgain', "Don't Show Again"); - const choice = await window.showWarningMessage( - localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."), - download, - neverShowAgain - ); - - if (choice === download) { - commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); - } else if (choice === neverShowAgain) { - await config.update('ignoreMissingGitWarning', true, true); - } + return new NoopAPIImpl(); } } -export function activate(context: ExtensionContext): API { - const config = workspace.getConfiguration('git', null); - const enabled = config.get('enabled'); - - const disposables: Disposable[] = []; - context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); - - const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string }; - telemetryReporter = new TelemetryReporter(name, version, aiKey); - - let activatePromise: Promise; - - if (enabled) { - activatePromise = _activate(context, disposables); - } else { - const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git')); - const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get('enabled') === true); - - activatePromise = eventToPromise(onEnabled) - .then(() => _activate(context, disposables)); - } - - const modelPromise = activatePromise - .then(model => model || Promise.reject('Git model not found')); - - activatePromise.catch(err => console.error(err)); - - return createApi(modelPromise); -} - async function checkGitVersion(info: IGit): Promise { const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLegacyWarning') === true; @@ -159,7 +152,3 @@ async function checkGitVersion(info: IGit): Promise { await config.update('ignoreLegacyWarning', true, true); } } - -export function deactivate(): Promise { - return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null); -} 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/grunt/README.md b/extensions/grunt/README.md index 34132a02dbe..b97ff21b117 100644 --- a/extensions/grunt/README.md +++ b/extensions/grunt/README.md @@ -1,10 +1,13 @@ # Grunt - The JavaScript Task Runner -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features This extension supports running [Grunt](https://gruntjs.com/) tasks defined in a `gruntfile.js` file as [VS Code tasks](https://code.visualstudio.com/docs/editor/tasks). Grunt tasks with the name 'build', 'compile', or 'watch' are treated as build tasks. -To run Grunt tasks use the `Tasks` menu. +To run Grunt tasks, use the **Tasks** menu. ## Settings -- `grunt.autoDetect` enable detecting tasks from `Gruntfile.js` files, the default is `on`. + +- `grunt.autoDetect` - Enable detecting tasks from `gruntfile.js` files, the default is `on`. diff --git a/extensions/gulp/README.md b/extensions/gulp/README.md index c06203d7a8d..42accc57541 100644 --- a/extensions/gulp/README.md +++ b/extensions/gulp/README.md @@ -1,10 +1,13 @@ # Gulp - Automate and enhance your workflow -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features This extension supports running [Gulp](https://gulpjs.com/) tasks defined in a `gulpfile.{js,ts}` file as [VS Code tasks](https://code.visualstudio.com/docs/editor/tasks). Gulp tasks with the name 'build', 'compile', or 'watch' are treated as build tasks. -To run Gulp tasks use the `Tasks` menu. +To run Gulp tasks, use the **Tasks** menu. ## Settings -- `gulp.autoDetect` enable detecting tasks from `gulpfile.{js,ts}` files, the default is `on`. + +- `gulp.autoDetect` - Enable detecting tasks from `gulpfile.{js,ts}` files, the default is `on`. diff --git a/extensions/html-language-features/CONTRIBUTING.md b/extensions/html-language-features/CONTRIBUTING.md new file mode 100644 index 00000000000..759abd46871 --- /dev/null +++ b/extensions/html-language-features/CONTRIBUTING.md @@ -0,0 +1,37 @@ +## Setup + +- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) +- Run `yarn` at `/`, this will install + - Dependencies for `/extension/html-language-features/` + - Dependencies for `/extension/html-language-features/server/` + - devDependencies such as `gulp` +- Open `/extensions/html-language-features/` as the workspace in VS Code +- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/html-language-features/.vscode/launch.json) debug target in the Debug View. This will: + - Launch the `preLaunchTask` task to compile the extension + - Launch a new VS Code instance with the `html-language-features` extension loaded + - You should see a notification saying the development version of `html-language-features` overwrites the bundled version of `html-language-features` +- Test the behavior of this extension by editing html files +- Run `Reload Window` command in the launched instance to reload the extension + +### Contribute to vscode-html-languageservice + +[Microsoft/vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice) contains the language smarts for html. +This extension wraps the html language service into a Language Server for VS Code. +If you want to fix html issues or make improvements, you should make changes at [Microsoft/vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice). + +However, within this extension, you can run a development version of `vscode-html-languageservice` to debug code or test language features interactively: + +#### Linking `vscode-html-languageservice` in `html-language-features/server/` + +- Clone [Microsoft/vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice) +- Run `yarn` in `vscode-html-languageservice` +- Run `yarn link` in `vscode-html-languageservice`. This will compile and link `vscode-html-languageservice` +- In `html-language-features/server/`, run `npm link vscode-html-languageservice` + +#### Testing the development version of `vscode-html-languageservice` + +- Open both `vscode-html-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature +- Run `yarn watch` at `html-languagefeatures/server/` to recompile this extension with the linked version of `vscode-html-languageservice` +- Make some changes in `vscode-html-languageservice` +- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-html-languageservice`. You can interactively test the language features. +- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-html-languageservice` and `html-language-features/server/` \ No newline at end of file diff --git a/extensions/html-language-features/README.md b/extensions/html-language-features/README.md new file mode 100644 index 00000000000..d2e78da0be9 --- /dev/null +++ b/extensions/html-language-features/README.md @@ -0,0 +1,9 @@ +# Language Features for HTML + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +See [HTML in Visual Studio Code](https://code.visualstudio.com/docs/languages/html) to learn about the features of this extension. + +Please read the [CONTRIBUTING.md](https://github.com/Microsoft/vscode/blob/master/extensions/html-language-features/CONTRIBUTING.md) file to learn how to contribute to this extension. diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index 4b59c6fba33..73c94b4acf6 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -1,6 +1,6 @@ { "displayName": "HTML Language Features", - "description": "Provides rich language support for HTML, Razor and Handlebar files.", + "description": "Provides rich language support for HTML, Razor, and Handlebar files", "html.format.enable.desc": "Enable/disable default HTML formatter", "html.format.wrapLineLength.desc": "Maximum amount of characters per line (0 = disable).", "html.format.unformatted.desc": "List of tags, comma separated, that shouldn't be reformatted. 'null' defaults to all tags listed at https://www.w3.org/TR/html5/dom.html#phrasing-content.", diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index 191253d0d2c..0c124f2d468 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -131,7 +131,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "beginCaptures": { "1": { "name": "meta.definition.variable.js entity.name.function.js" @@ -577,7 +581,7 @@ } }, { - "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "storage.modifier.js" @@ -809,7 +813,7 @@ "include": "#comment" }, { - "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "meta.definition.property.js entity.name.function.js" @@ -2163,7 +2167,7 @@ }, { "name": "meta.object.member.js", - "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "0": { "name": "meta.object-literal.key.js" @@ -2408,7 +2412,7 @@ ] }, { - "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\(\\s*$)", + "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\()|(<))\\s*$)", "beginCaptures": { "1": { "name": "storage.modifier.async.js" @@ -3000,7 +3004,7 @@ "include": "#object-identifiers" }, { - "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", + "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.js" @@ -3470,7 +3474,7 @@ "include": "#destructuring-parameter" }, { - "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)))", + "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))))", "captures": { "1": { "name": "storage.modifier.js" @@ -4604,7 +4608,7 @@ ] }, "jsx-tag-without-attributes-in-expression": { - "begin": "(?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", + "begin": "(?:*]|&&|\\|\\||\\?|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "patterns": [ { @@ -4664,7 +4668,7 @@ ] }, "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.]*)(?))", + "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\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": [ { diff --git a/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json b/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json index a7ceac55af3..8015ed5e8ed 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/88217b1c7d36ed5d35adc3099ba7978aabe2531b", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/27425437b2144f43607047ae7ee9b826e36856a5", "name": "JavaScript (with React support)", "scopeName": "source.js.jsx", "patterns": [ @@ -139,6 +139,10 @@ "name": "keyword.control.with.js.jsx", "match": "(?)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "beginCaptures": { "1": { "name": "meta.definition.variable.js.jsx entity.name.function.js.jsx" @@ -577,7 +581,7 @@ } }, { - "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "storage.modifier.js.jsx" @@ -809,7 +813,7 @@ "include": "#comment" }, { - "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "meta.definition.property.js.jsx entity.name.function.js.jsx" @@ -2163,7 +2167,7 @@ }, { "name": "meta.object.member.js.jsx", - "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "0": { "name": "meta.object-literal.key.js.jsx" @@ -2408,7 +2412,7 @@ ] }, { - "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\(\\s*$)", + "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\()|(<))\\s*$)", "beginCaptures": { "1": { "name": "storage.modifier.async.js.jsx" @@ -3000,7 +3004,7 @@ "include": "#object-identifiers" }, { - "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", + "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.js.jsx" @@ -3470,7 +3474,7 @@ "include": "#destructuring-parameter" }, { - "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)))", + "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))))", "captures": { "1": { "name": "storage.modifier.js.jsx" @@ -4604,7 +4608,7 @@ ] }, "jsx-tag-without-attributes-in-expression": { - "begin": "(?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", + "begin": "(?:*]|&&|\\|\\||\\?|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "patterns": [ { @@ -4664,7 +4668,7 @@ ] }, "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.]*)(?))", + "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\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": [ { diff --git a/extensions/json-language-features/README.md b/extensions/json-language-features/README.md new file mode 100644 index 00000000000..2ff5e6e57d3 --- /dev/null +++ b/extensions/json-language-features/README.md @@ -0,0 +1,7 @@ +# Language Features for JSON files + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +See [JSON in Visual Studio Code](https://code.visualstudio.com/docs/languages/json) to learn about the features of this extension. \ No newline at end of file diff --git a/extensions/make/package.json b/extensions/make/package.json index d57f0077caa..f66762ba800 100644 --- a/extensions/make/package.json +++ b/extensions/make/package.json @@ -4,29 +4,47 @@ "description": "%description%", "version": "1.0.0", "publisher": "vscode", - "engines": { "vscode": "*" }, + "engines": { + "vscode": "*" + }, "scripts": { "update-grammar": "node ../../build/npm/update-grammar.js fadeevab/make.tmbundle Syntaxes/Makefile.plist ./syntaxes/make.tmLanguage.json" }, "contributes": { - - "languages": [{ - "id": "makefile", - "aliases": ["Makefile", "makefile"], - "extensions": [ ".mk" ], - "filenames": [ "Makefile", "makefile", "GNUmakefile", "OCamlMakefile" ], - "firstLine": "^#!\\s*/usr/bin/make", - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "makefile", - "scopeName": "source.makefile", - "path": "./syntaxes/make.tmLanguage.json" - }], + "languages": [ + { + "id": "makefile", + "aliases": [ + "Makefile", + "makefile" + ], + "extensions": [ + ".mk" + ], + "filenames": [ + "Makefile", + "makefile", + "GNUmakefile", + "OCamlMakefile" + ], + "firstLine": "^#!\\s*/usr/bin/make", + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "makefile", + "scopeName": "source.makefile", + "path": "./syntaxes/make.tmLanguage.json", + "tokenTypes": { + "string.interpolated": "other" + } + } + ], "configurationDefaults": { "[makefile]": { "editor.insertSpaces": false } } } -} +} \ No newline at end of file diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index a091ad8ea29..dd0b3f5a87d 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -19,7 +19,8 @@ ".md", ".mdown", ".markdown", - ".markdn" + ".markdn", + ".workbook" ], "configuration": "./language-configuration.json" } diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index 2838349491e..d49d0688467 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.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/vscode-markdown-tm-grammar/commit/fc14af6c0ca3c2bdaa1681f6f51834fe6a0a4279", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/4504240cdb13a4640f64fc98a0adb858226a879e", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -722,7 +722,7 @@ "contentName": "meta.embedded.block.dosbatch", "patterns": [ { - "include": "source.dosbatch" + "include": "source.batchfile" } ] } diff --git a/extensions/markdown-language-features/README.md b/extensions/markdown-language-features/README.md new file mode 100644 index 00000000000..e80e9e886bb --- /dev/null +++ b/extensions/markdown-language-features/README.md @@ -0,0 +1,7 @@ +# Language Features for Markdown files + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +See [Markdown in Visual Studio Code](https://code.visualstudio.com/docs/languages/markdown) to learn about the features of this extension. \ No newline at end of file diff --git a/extensions/markdown-language-features/src/slugify.ts b/extensions/markdown-language-features/src/slugify.ts index c3e167e89e0..4bc3cee42ce 100644 --- a/extensions/markdown-language-features/src/slugify.ts +++ b/extensions/markdown-language-features/src/slugify.ts @@ -23,7 +23,7 @@ export const githubSlugifier: Slugifier = new class implements Slugifier { heading.trim() .toLowerCase() .replace(/\s+/g, '-') // Replace whitespace with - - .replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '') // Remove known puctuators + .replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known puctuators .replace(/^\-+/, '') // Remove leading - .replace(/\-+$/, '') // Remove trailing - ); diff --git a/extensions/merge-conflict/README.md b/extensions/merge-conflict/README.md index a2f874bcb42..8077ecd6517 100644 --- a/extensions/merge-conflict/README.md +++ b/extensions/merge-conflict/README.md @@ -1,5 +1,7 @@ # Merge Conflict -See [documentation](https://code.visualstudio.com/docs/editor/versioncontrol#_merge-conflicts). +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. -**Notice** This is a an extension that is bundled with Visual Studio Code. +## Features + +See [Merge Conflicts in VS Code](https://code.visualstudio.com/docs/editor/versioncontrol#_merge-conflicts) to learn about features of this extension. diff --git a/extensions/npm/README.md b/extensions/npm/README.md index 625002ee7ee..c3dd9437203 100644 --- a/extensions/npm/README.md +++ b/extensions/npm/README.md @@ -1,18 +1,33 @@ # Node npm -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +### Task Running This extension supports running npm scripts defined in the `package.json` as [tasks](https://code.visualstudio.com/docs/editor/tasks). Scripts with the name 'build', 'compile', or 'watch' are treated as build tasks. -To run scripts as tasks you use the `Tasks` menu. +To run scripts as tasks, use the **Tasks** menu. -For more information about auto detection of Tasks pls see the [documentation](https://code.visualstudio.com/Docs/editor/tasks#_task-autodetection). +For more information about auto detection of Tasks, see the [documentation](https://code.visualstudio.com/Docs/editor/tasks#_task-autodetection). + +### Script Explorer + +The Npm Script Explorer shows the npm scripts found in your workspace. The explorer view is enabled by the setting `npm.enableScriptExplorer`. A script can be opened, run, or debug from the explorer. + +### Run Scripts from the Editor + +The extension provides code lense actions to run or debug a script from the editor. ## Settings -- `npm.autoDetect` enable detecting scripts as tasks, the default is `on`. -- `npm.runSilent` run npm script with the `--silent` option, the default is `false`. -- `npm.packageManager` the package manager used to run the scripts: `npm` or `yarn`, the default is `npm`. -- `npm.exclude` glob patterns for folders that should be excluded from automatic script detection. The pattern is matched against the **absolute path** of the package.json. For example, to exclude all test folders use '**/test/**'. -- `npm.enableScriptExplorer` enable an explorer view for npm scripts. -- `npm.scriptExplorerAction` the default click action: `open` or `run`, the default is `open`. + +- `npm.autoDetect` - Enable detecting scripts as tasks, the default is `on`. +- `npm.runSilent` - Run npm script with the `--silent` option, the default is `false`. +- `npm.packageManager` - The package manager used to run the scripts: `npm` or `yarn`, the default is `npm`. +- `npm.exclude` - Glob patterns for folders that should be excluded from automatic script detection. The pattern is matched against the **absolute path** of the package.json. For example, to exclude all test folders use '**/test/**'. +- `npm.enableScriptExplorer` - Enable an explorer view for npm scripts. +- `npm.scriptExplorerAction` - The default click action: `open` or `run`, the default is `open`. +- `npm.scriptCodeLens.enable` - Enable/disable the code lenses to run a script. + diff --git a/extensions/npm/package.json b/extensions/npm/package.json index df9c903fcb4..7f4b794ee0c 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -178,6 +178,12 @@ "scope": "resource", "description": "%config.npm.runSilent%" }, + "npm.scriptCodeLens.enable": { + "type": "boolean", + "default": true, + "scope": "resource", + "description": "%config.scriptCodeLens.enable%" + }, "npm.packageManager": { "scope": "resource", "type": "string", diff --git a/extensions/npm/package.nls.json b/extensions/npm/package.nls.json index 92665d5f65a..be9381e3377 100644 --- a/extensions/npm/package.nls.json +++ b/extensions/npm/package.nls.json @@ -7,6 +7,7 @@ "config.npm.exclude": "Configure glob patterns for folders that should be excluded from automatic script detection.", "config.npm.enableScriptExplorer": "Enable an explorer view for npm scripts.", "config.npm.scriptExplorerAction": "The default click action used in the scripts explorer: 'open' or 'run', the default is 'open'.", + "config.scriptCodeLens.enable": "Enable the code lens to 'Run' or 'Debug' an npm script.", "npm.parseError": "Npm task detection: failed to parse the file {0}", "taskdef.script": "The npm script to customize.", "taskdef.path": "The path to the folder of the package.json file that provides the script. Can be omitted.", diff --git a/extensions/npm/src/lenses.ts b/extensions/npm/src/lenses.ts new file mode 100644 index 00000000000..6394fbad5b3 --- /dev/null +++ b/extensions/npm/src/lenses.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + ExtensionContext, CodeLensProvider, TextDocument, commands, ProviderResult, CodeLens, CancellationToken, + workspace, tasks, Range, Command, Event, EventEmitter +} from 'vscode'; +import { + createTask, startDebugging, findAllScriptRanges, extractDebugArgFromScript +} from './tasks'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class NpmLensProvider implements CodeLensProvider { + private extensionContext: ExtensionContext; + private _onDidChangeCodeLenses: EventEmitter = new EventEmitter(); + readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event; + + constructor(context: ExtensionContext) { + this.extensionContext = context; + context.subscriptions.push(commands.registerCommand('npm.runScriptFromLens', this.runScriptFromLens, this)); + context.subscriptions.push(commands.registerCommand('npm.debugScriptFromLens', this.debugScriptFromLens, this)); + } + + public provideCodeLenses(document: TextDocument, _token: CancellationToken): ProviderResult { + let result = findAllScriptRanges(document.getText()); + let folder = workspace.getWorkspaceFolder(document.uri); + let lenses: CodeLens[] = []; + + + if (folder && !workspace.getConfiguration('npm', folder.uri).get('scriptCodeLens.enable', 'true')) { + return lenses; + } + + result.forEach((value, key) => { + let start = document.positionAt(value[0]); + let end = document.positionAt(value[0] + value[1]); + let range = new Range(start, end); + + let command: Command = { + command: 'npm.runScriptFromLens', + title: localize('run', "Run"), + arguments: [document, key] + }; + let lens: CodeLens = new CodeLens(range, command); + lenses.push(lens); + + let debugArgs = extractDebugArgFromScript(value[2]); + if (debugArgs) { + command = { + command: 'npm.debugScriptFromLens', + title: localize('debug', "Debug"), + arguments: [document, key, debugArgs[0], debugArgs[1]] + }; + lens = new CodeLens(range, command); + lenses.push(lens); + } + }); + return lenses; + } + + public refresh() { + this._onDidChangeCodeLenses.fire(); + } + + public runScriptFromLens(document: TextDocument, script: string) { + let uri = document.uri; + let folder = workspace.getWorkspaceFolder(uri); + if (folder) { + let task = createTask(script, `run ${script}`, folder, uri); + tasks.executeTask(task); + } + } + + public debugScriptFromLens(document: TextDocument, script: string, protocol: string, port: number) { + let uri = document.uri; + let folder = workspace.getWorkspaceFolder(uri); + if (folder) { + startDebugging(script, protocol, port, folder); + } + } +} diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/main.ts index 023fddbffb6..f2c80f5f476 100644 --- a/extensions/npm/src/main.ts +++ b/extensions/npm/src/main.ts @@ -9,13 +9,14 @@ import * as vscode from 'vscode'; import { addJSONProviders } from './features/jsonContributions'; import { NpmScriptsTreeDataProvider } from './npmView'; -import { provideNpmScripts, invalidateScriptsCache } from './tasks'; - -let taskProvider: vscode.Disposable | undefined; +import { invalidateScriptsCache, NpmTaskProvider } from './tasks'; +import { NpmLensProvider } from './lenses'; export async function activate(context: vscode.ExtensionContext): Promise { - taskProvider = registerTaskProvider(context); + const taskProvider = registerTaskProvider(context); const treeDataProvider = registerExplorer(context); + const lensProvider = registerLensProvider(context); + configureHttpRequest(); vscode.workspace.onDidChangeConfiguration((e) => { configureHttpRequest(); @@ -30,6 +31,11 @@ export async function activate(context: vscode.ExtensionContext): Promise treeDataProvider.refresh(); } } + if (e.affectsConfiguration('npm.scriptCodeLens.enable')) { + if (lensProvider) { + lensProvider.refresh(); + } + } }); context.subscriptions.push(addJSONProviders(httpRequest.xhr)); } @@ -42,15 +48,10 @@ function registerTaskProvider(context: vscode.ExtensionContext): vscode.Disposab watcher.onDidCreate((_e) => invalidateScriptsCache()); context.subscriptions.push(watcher); - let provider: vscode.TaskProvider = { - provideTasks: async () => { - return provideNpmScripts(); - }, - resolveTask(_task: vscode.Task): vscode.Task | undefined { - return undefined; - } - }; - return vscode.workspace.registerTaskProvider('npm', provider); + let provider: vscode.TaskProvider = new NpmTaskProvider(context); + let disposable = vscode.workspace.registerTaskProvider('npm', provider); + context.subscriptions.push(disposable); + return disposable; } return undefined; } @@ -65,13 +66,24 @@ function registerExplorer(context: vscode.ExtensionContext): NpmScriptsTreeDataP return undefined; } +function registerLensProvider(context: vscode.ExtensionContext): NpmLensProvider | undefined { + if (vscode.workspace.workspaceFolders) { + let npmSelector: vscode.DocumentSelector = { + language: 'json', + scheme: 'file', + pattern: '**/package.json' + }; + let provider = new NpmLensProvider(context); + context.subscriptions.push(vscode.languages.registerCodeLensProvider(npmSelector, provider)); + return provider; + } + return undefined; +} + function configureHttpRequest() { const httpSettings = vscode.workspace.getConfiguration('http'); httpRequest.configure(httpSettings.get('proxy', ''), httpSettings.get('proxyStrictSSL', true)); } export function deactivate(): void { - if (taskProvider) { - taskProvider.dispose(); - } } diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts index d4d5279e9a4..f737df23e1b 100644 --- a/extensions/npm/src/npmView.ts +++ b/extensions/npm/src/npmView.ts @@ -6,14 +6,14 @@ import * as path from 'path'; import { - DebugConfiguration, Event, EventEmitter, ExtensionContext, Task, + Event, EventEmitter, ExtensionContext, Task, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, - WorkspaceFolder, commands, debug, window, workspace, tasks, Selection, TaskGroup + WorkspaceFolder, commands, window, workspace, tasks, Selection, TaskGroup } from 'vscode'; import { visit, JSONVisitor } from 'jsonc-parser'; import { NpmTaskDefinition, getPackageJsonUriFromTask, getScripts, - isWorkspaceFolder, getPackageManager, getTaskName, createTask + isWorkspaceFolder, getTaskName, createTask, extractDebugArgFromScript, startDebugging } from './tasks'; import * as nls from 'vscode-nls'; @@ -162,25 +162,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { } private extractDebugArg(scripts: any, task: Task): [string, number] | undefined { - let script: string = scripts[task.name]; - - // 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[6]) { - return [match[1], parseInt(match[6])]; - } - if (match[1] === 'inspect') { - return [match[1], 9229]; - } - if (match[1] === 'debug') { - return [match[1], 5858]; - } - } - return undefined; + return extractDebugArgFromScript(scripts[task.name]); } private async debugScript(script: NpmScript) { @@ -193,7 +175,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { return; } - let debugArg = await this.extractDebugArg(scripts, task); + let debugArg = this.extractDebugArg(scripts, task); if (!debugArg) { let message = localize('noDebugOptions', 'Could not launch "{0}" for debugging because the scripts lacks a node debug option, e.g. "--inspect-brk".', task.name); let learnMore = localize('learnMore', 'Learn More'); @@ -204,29 +186,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { } return; } - - let protocol = 'inspector'; - if (debugArg[0] === 'debug') { - protocol = 'legacy'; - } - - let packageManager = getPackageManager(script.getFolder()); - const config: DebugConfiguration = { - type: 'node', - request: 'launch', - name: `Debug ${task.name}`, - runtimeExecutable: packageManager, - runtimeArgs: [ - 'run-script', - task.name, - ], - port: debugArg[1], - protocol: protocol - }; - - if (isWorkspaceFolder(task.scope)) { - debug.startDebugging(task.scope, config); - } + startDebugging(task.name, debugArg[0], debugArg[1], script.getFolder()); } private scriptNotValid(task: Task) { @@ -358,7 +318,6 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { private buildTaskTree(tasks: Task[]): Folder[] | PackageJSON[] | NoScripts[] { let folders: Map = new Map(); let packages: Map = new Map(); - let scripts: Map = new Map(); let folder = null; let packageJson = null; @@ -380,11 +339,8 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { packages.set(fullPath, packageJson); } let fullScriptPath = path.join(packageJson.path, each.name); - if (!scripts.get(fullScriptPath)) { - let script = new NpmScript(this.extensionContext, packageJson, each); - packageJson.addScript(script); - scripts.set(fullScriptPath, script); - } + let script = new NpmScript(this.extensionContext, packageJson, each); + packageJson.addScript(script); } }); if (folders.size === 1) { diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index 88fa3c82824..5d19a4c4c00 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -4,7 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace } from 'vscode'; +import { + TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace, + DebugConfiguration, debug, TaskProvider, ExtensionContext +} from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import * as minimatch from 'minimatch'; @@ -22,6 +25,22 @@ type AutoDetect = 'on' | 'off'; let cachedTasks: Task[] | undefined = undefined; +export class NpmTaskProvider implements TaskProvider { + private extensionContext: ExtensionContext; + + constructor(context: ExtensionContext) { + this.extensionContext = context; + } + + public provideTasks() { + return provideNpmScripts(); + } + + public resolveTask(_task: Task): Task | undefined { + return undefined; + } +} + export function invalidateScriptsCache() { cachedTasks = undefined; } @@ -100,6 +119,7 @@ async function detectNpmScripts(): Promise { let emptyTasks: Task[] = []; let allTasks: Task[] = []; + let visitedPackageJsonFiles: Set = new Set(); let folders = workspace.workspaceFolders; if (!folders) { @@ -112,8 +132,10 @@ async function detectNpmScripts(): Promise { let relativePattern = new RelativePattern(folder, '**/package.json'); let paths = await workspace.findFiles(relativePattern, '**/node_modules/**'); for (let j = 0; j < paths.length; j++) { - if (!isExcluded(folder, paths[j])) { - let tasks = await provideNpmScriptsForFolder(paths[j]); + let path = paths[j]; + if (!isExcluded(folder, path) && !visitedPackageJsonFiles.has(path.fsPath)) { + let tasks = await provideNpmScriptsForFolder(path); + visitedPackageJsonFiles.add(path.fsPath); allTasks.push(...tasks); } } @@ -159,7 +181,7 @@ function isExcluded(folder: WorkspaceFolder, packageJsonUri: Uri) { } function isDebugScript(script: string): boolean { - let match = script.match(/--(inspect|debug)(-brk)?(=(\d*))?/); + let match = script.match(/--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/); return match !== null; } @@ -266,6 +288,52 @@ async function readFile(file: string): Promise { }); } +export function extractDebugArgFromScript(scriptValue: string): [string, number] | undefined { + // 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 = scriptValue.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[6]) { + return [match[1], parseInt(match[6])]; + } + if (match[1] === 'inspect') { + return [match[1], 9229]; + } + if (match[1] === 'debug') { + return [match[1], 5858]; + } + } + return undefined; +} + +export function startDebugging(scriptName: string, protocol: string, port: number, folder: WorkspaceFolder) { + let p = 'inspector'; + if (protocol === 'debug') { + p = 'legacy'; + } + + let packageManager = getPackageManager(folder); + const config: DebugConfiguration = { + type: 'node', + request: 'launch', + name: `Debug ${scriptName}`, + runtimeExecutable: packageManager, + runtimeArgs: [ + 'run-script', + scriptName, + ], + port: port, + protocol: p + }; + + if (folder) { + debug.startDebugging(folder, config); + } +} + + export type StringMap = { [s: string]: string; }; async function findAllScripts(buffer: string): Promise { @@ -275,7 +343,6 @@ async function findAllScripts(buffer: string): Promise { let visitor: JSONVisitor = { onError(_error: ParseErrorCode, _offset: number, _length: number) { - // TODO: inform user about the parse error }, onObjectEnd() { if (inScripts) { @@ -301,6 +368,39 @@ async function findAllScripts(buffer: string): Promise { return scripts; } +export function findAllScriptRanges(buffer: string): Map { + var scripts: Map = new Map(); + let script: string | undefined = undefined; + let inScripts = false; + + let visitor: JSONVisitor = { + onError(_error: ParseErrorCode, _offset: number, _length: number) { + }, + onObjectEnd() { + if (inScripts) { + inScripts = false; + } + }, + onLiteralValue(value: any, offset: number, length: number) { + if (script) { + scripts.set(script, [offset, length, value]); + script = undefined; + } + }, + onObjectProperty(property: string, offset: number, length: number) { + if (property === 'scripts') { + inScripts = true; + } + else if (inScripts) { + script = property; + } + } + }; + visit(buffer, visitor); + return scripts; +} + + export async function getScripts(packageJsonUri: Uri): Promise { if (packageJsonUri.scheme !== 'file') { diff --git a/extensions/package.json b/extensions/package.json index c7389b534c4..7b4fd5a5bd0 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,9 +3,9 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "2.9.2" + "typescript": "3.0.1-insiders.20180713" }, "scripts": { "postinstall": "node ./postinstall" } -} \ No newline at end of file +} diff --git a/extensions/php-language-features/README.md b/extensions/php-language-features/README.md new file mode 100644 index 00000000000..c00be6a964e --- /dev/null +++ b/extensions/php-language-features/README.md @@ -0,0 +1,7 @@ +# Language Features for PHP files + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +See [PHP in Visual Studio Code](https://code.visualstudio.com/docs/languages/php) to learn about the features of this extension. \ No newline at end of file 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 b149088fcf2..d798d7b2842 100644 --- a/extensions/search-rg/package.json +++ b/extensions/search-rg/package.json @@ -24,6 +24,7 @@ }, "scripts": {}, "activationEvents": [ + "onSearch:file", "*" ], "main": "./out/extension", diff --git a/extensions/search-rg/src/cachedSearchProvider.ts b/extensions/search-rg/src/cachedSearchProvider.ts index 8dba32717e1..ec28b6b6a81 100644 --- a/extensions/search-rg/src/cachedSearchProvider.ts +++ b/extensions/search-rg/src/cachedSearchProvider.ts @@ -74,8 +74,13 @@ export class CachedSearchProvider { } return allResultsPromise.then(results => { - const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); - return this.sortResults(args, results, scorerCache); + // TODO@roblou quickopen results are not scored until the first keypress + if (args.query.pattern) { + const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); + return this.sortResults(args, results, scorerCache); + } else { + return results; + } }); } diff --git a/extensions/search-rg/src/ripgrepFileSearch.ts b/extensions/search-rg/src/ripgrepFileSearch.ts index 7edf4c4f4b9..fd3b6acc99a 100644 --- a/extensions/search-rg/src/ripgrepFileSearch.ts +++ b/extensions/search-rg/src/ripgrepFileSearch.ts @@ -114,16 +114,22 @@ export class RipgrepFileSearchEngine implements IInternalFileSearchProvider { cb(err, stdout, last); }; + let gotData = false; if (cmd.stdout) { + // Should be non-null, but #38195 this.forwardData(cmd.stdout, onData); + cmd.stdout.once('data', () => gotData = true); } else { this.outputChannel.appendLine('stdout is null'); } - const stderr = this.collectData(cmd.stderr); - - let gotData = false; - cmd.stdout.once('data', () => gotData = true); + let stderr: Buffer[]; + if (cmd.stderr) { + // Should be non-null, but #38195 + stderr = this.collectData(cmd.stderr); + } else { + this.outputChannel.appendLine('stderr is null'); + } cmd.on('error', (err: Error) => { onData(err); diff --git a/extensions/theme-defaults/themes/dark_defaults.json b/extensions/theme-defaults/themes/dark_defaults.json index 0feebc56060..c2128772b36 100644 --- a/extensions/theme-defaults/themes/dark_defaults.json +++ b/extensions/theme-defaults/themes/dark_defaults.json @@ -12,5 +12,7 @@ "activityBarBadge.background": "#007ACC", "sideBarTitle.foreground": "#BBBBBB", "input.placeholderForeground": "#A6A6A6" + "settings.textInputBackground": "#292929", + "settings.numberInputBackground": "#292929" } } \ No newline at end of file diff --git a/extensions/theme-defaults/themes/light_defaults.json b/extensions/theme-defaults/themes/light_defaults.json index 17f154862a1..dc53cbfb1ea 100644 --- a/extensions/theme-defaults/themes/light_defaults.json +++ b/extensions/theme-defaults/themes/light_defaults.json @@ -12,6 +12,8 @@ "activityBarBadge.background": "#007ACC", "sideBarTitle.foreground": "#6F6F6F", "list.hoverBackground": "#E8E8E8", - "input.placeholderForeground": "#767676" + "input.placeholderForeground": "#767676", + "settings.textInputBorder": "#CECECE", + "settings.numberInputBorder": "#CECECE" } } \ No newline at end of file diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index 9a93b5e73e9..41d9d0b2d8e 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -3,6 +3,7 @@ "type": "dark", "colors": { "input.background": "#51412c", + "dropdown.background": "#51412c", "editor.background": "#221a0f", "editor.foreground": "#d3af86", "focusBorder": "#a57a4c", diff --git a/extensions/theme-monokai/themes/monokai-color-theme.json b/extensions/theme-monokai/themes/monokai-color-theme.json index 7321af7b015..e7f00d712e6 100644 --- a/extensions/theme-monokai/themes/monokai-color-theme.json +++ b/extensions/theme-monokai/themes/monokai-color-theme.json @@ -11,6 +11,8 @@ "list.activeSelectionBackground": "#75715E", "list.focusBackground": "#414339", "dropdown.listBackground": "#1e1f1c", + "settings.textInputBackground": "#32342d", + "settings.numberInputBackground": "#32342d", "list.inactiveSelectionBackground": "#414339", "list.hoverBackground": "#3e3d32", "list.dropBackground": "#414339", diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index bee4c021973..efd1eed74e0 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/40288b872220e5c0b844b1de507f1749ed14589b", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/27425437b2144f43607047ae7ee9b826e36856a5", "name": "TypeScript", "scopeName": "source.ts", "patterns": [ @@ -139,6 +139,10 @@ "name": "keyword.control.with.ts", "match": "(?)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "beginCaptures": { "1": { "name": "meta.definition.variable.ts entity.name.function.ts" @@ -574,7 +578,7 @@ } }, { - "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "storage.modifier.ts" @@ -806,7 +810,7 @@ "include": "#comment" }, { - "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "meta.definition.property.ts entity.name.function.ts" @@ -2160,7 +2164,7 @@ }, { "name": "meta.object.member.ts", - "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "0": { "name": "meta.object-literal.key.ts" @@ -2405,7 +2409,7 @@ ] }, { - "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\(\\s*$)", + "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\()|(<))\\s*$)", "beginCaptures": { "1": { "name": "storage.modifier.async.ts" @@ -2488,7 +2492,7 @@ "patterns": [ { "name": "cast.expr.ts", - "begin": "(?:(?*?\\&\\|\\^]|[^_$[:alnum:]](?:\\+\\+|\\-\\-)|[^\\+]\\+|[^\\-]\\-))\\s*(<)(?!*?\\&\\|\\^]|[^_$[:alnum:]](?:\\+\\+|\\-\\-)|[^\\+]\\+|[^\\-]\\-))\\s*(<)(?!)\n )) |\n ((async\\s*)?(\n ((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", + "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.ts" @@ -3504,7 +3508,7 @@ "include": "#destructuring-parameter" }, { - "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$)))", + "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?[\\(]\\s*([\\{\\[]\\s*)?$))))", "captures": { "1": { "name": "storage.modifier.ts" diff --git a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json index 441f07d6abd..8d1c10a62b7 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/88217b1c7d36ed5d35adc3099ba7978aabe2531b", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/27425437b2144f43607047ae7ee9b826e36856a5", "name": "TypeScriptReact", "scopeName": "source.tsx", "patterns": [ @@ -139,6 +139,10 @@ "name": "keyword.control.with.tsx", "match": "(?)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "begin": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "beginCaptures": { "1": { "name": "meta.definition.variable.tsx entity.name.function.tsx" @@ -577,7 +581,7 @@ } }, { - "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:(?)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "storage.modifier.tsx" @@ -809,7 +813,7 @@ "include": "#comment" }, { - "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)([_$[:alpha:]][_$[:alnum:]]*)(\\?)?(?=(\\?\\s*)?\\s*\n# function assignment |\n(=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)) |\n# typeannotation is fn type: < | () | (... | (param: | (param, | (param? | (param= | (param) =>\n(:\\s*(\n (<) |\n ([(]\\s*(\n ([)]) |\n (\\.\\.\\.) |\n ([_$[:alnum:]]+\\s*(\n ([:,?=])|\n ([)]\\s*=>)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))) |\n(:\\s*(=>|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(<[^<>]*>)|[^<>(),=])+=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "1": { "name": "meta.definition.property.tsx entity.name.function.tsx" @@ -2163,7 +2167,7 @@ }, { "name": "meta.object.member.tsx", - "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", + "match": "(?x)(?:([_$[:alpha:]][_$[:alnum:]]*)\\s*(?=:\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n)))", "captures": { "0": { "name": "meta.object-literal.key.tsx" @@ -2408,7 +2412,7 @@ ] }, { - "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\(\\s*$)", + "begin": "(?<=[(=,]|=>)\\s*(async)?(?=\\s*((((<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*))?\\()|(<))\\s*$)", "beginCaptures": { "1": { "name": "storage.modifier.async.tsx" @@ -3000,7 +3004,7 @@ "include": "#object-identifiers" }, { - "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ([\\(]\\s*([\\{\\[]\\s*)?$) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", + "match": "(?x)(?:(?:(\\.)|(\\?\\.(?!\\s*[[:digit:]])))\\s*)?([_$[:alpha:]][_$[:alnum:]]*)(?=\\s*=\\s*(\n ((async\\s+)?(\n (function\\s*[(<*]) |\n (function\\s+) |\n ([_$[:alpha:]][_$[:alnum:]]*\\s*=>)\n )) |\n ((async\\s*)?(\n ((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$)) |\n # sure shot arrow functions even if => is on new line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)?\n [(]\\s*\n (\n ([)]\\s*:) | # ():\n ((\\.\\.\\.\\s*)?[_$[:alpha:]][_$[:alnum:]]*\\s*:) # [(]param: | [(]...param:\n )\n) |\n(\n [<]\\s*[_$[:alpha:]][_$[:alnum:]]*\\s+extends\\s*[^=>] # < typeparam extends\n) |\n# arrow function possible to detect only with => on same line\n(\n (<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<]|\\<\\s*([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\))|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\]))([^=<>]|=[^<])*\\>)*>\\s*)? # typeparameters\n \\(\\s*(([_$[:alpha:]]|(\\{([^\\{\\}]|(\\{[^\\{\\}]*\\}))*\\})|(\\[([^\\[\\]]|(\\[[^\\[\\]]*\\]))*\\])|(\\.\\.\\.\\s*[_$[:alpha:]]))([^()]|(\\(([^\\(\\)]|(\\([^\\(\\)]*\\)))*\\)))*)?\\) # parameters\n (\\s*:\\s*([^<>\\(\\)]|\\<[^<>]+\\>|\\([^\\(\\)]+\\))+)? # return type\n \\s*=> # arrow operator\n)\n ))\n))", "captures": { "1": { "name": "punctuation.accessor.tsx" @@ -3470,7 +3474,7 @@ "include": "#destructuring-parameter" }, { - "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*([\\(]\\s*([\\{\\[]\\s*)?$)))", + "match": "(?x)(?:(?)\n ))\n ))\n)) |\n(:\\s*((<\\s*$)|([\\(]\\s*([\\{\\[]\\s*)?$))))", "captures": { "1": { "name": "storage.modifier.tsx" @@ -4604,7 +4608,7 @@ ] }, "jsx-tag-without-attributes-in-expression": { - "begin": "(?:*]|&&|\\|\\||\\?|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^)\\s*(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", + "begin": "(?:*]|&&|\\|\\||\\?|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*(?=(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "end": "(?!(<)\\s*(?:([_$a-zA-Z][-$\\w.]*)(?))", "patterns": [ { @@ -4664,7 +4668,7 @@ ] }, "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.]*)(?))", + "begin": "(?x)\n (?:*]|&&|\\|\\||\\?|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\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": [ { diff --git a/extensions/typescript-language-features/OSSREADME.json b/extensions/typescript-language-features/OSSREADME.json index 160da003cd8..d36b840718e 100644 --- a/extensions/typescript-language-features/OSSREADME.json +++ b/extensions/typescript-language-features/OSSREADME.json @@ -1,7 +1,164 @@ -[{ - "name": "TypeScript-TmLanguage", - "version": "0.1.8", - "license": "MIT", - "repositoryURL": "https://github.com/Microsoft/TypeScript-TmLanguage", - "description": "The files syntaxes/TypeScript.tmLanguage.json and syntaxes/TypeScriptReact.tmLanguage.json were derived from TypeScript.tmLanguage and TypeScriptReact.tmLanguage in https://github.com/Microsoft/TypeScript-TmLanguage." -}] +[ + { + "name": "TypeScript-TmLanguage", + "version": "0.1.8", + "license": "MIT", + "repositoryURL": "https://github.com/Microsoft/TypeScript-TmLanguage", + "description": "The files syntaxes/TypeScript.tmLanguage.json and syntaxes/TypeScriptReact.tmLanguage.json were derived from TypeScript.tmLanguage and TypeScriptReact.tmLanguage in https://github.com/Microsoft/TypeScript-TmLanguage." + }, + { + "name": "DefinitelyTyped", + "version": "0.0.2", + "license": "MIT", + "repositoryURL": "https://github.com/DefinitelyTyped/DefinitelyTyped", + "description": "Typings files that are downloaded by TypeScript. These typings power IntelliSense for JavaScript and TypeScript.", + "licenseDetail": [ + "MIT License", + "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.", + ] + }, + { + "name": "Unicode", + "license": "UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE", + "description": "These files are included by TypeScript.Ø", + "licenseDetail": [ + "Unicode Data Files include all data files under the directories", + "http://www.unicode.org/Public/, http://www.unicode.org/reports/,", + "http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and", + "http://www.unicode.org/utility/trac/browser/.", + "", + "Unicode Data Files do not include PDF online code charts under the", + "directory http://www.unicode.org/Public/.", + "", + "Software includes any source code published in the Unicode Standard", + "or under the directories", + "http://www.unicode.org/Public/, http://www.unicode.org/reports/,", + "http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and", + "http://www.unicode.org/utility/trac/browser/.", + "", + "NOTICE TO USER: Carefully read the following legal agreement.", + "BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S", + "DATA FILES (\"DATA FILES\"), AND/OR SOFTWARE (\"SOFTWARE\"),", + "YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE", + "TERMS AND CONDITIONS OF THIS AGREEMENT.", + "IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE", + "THE DATA FILES OR SOFTWARE.", + "", + "COPYRIGHT AND PERMISSION NOTICE", + "", + "Copyright (c) 1991-2017 Unicode, Inc. All rights reserved.", + "Distributed under the Terms of Use in http://www.unicode.org/copyright.html.", + "", + "Permission is hereby granted, free of charge, to any person obtaining", + "a copy of the Unicode data files and any associated documentation", + "(the \"Data Files\") or Unicode software and any associated documentation", + "(the \"Software\") to deal in the Data Files or Software", + "without restriction, including without limitation the rights to use,", + "copy, modify, merge, publish, distribute, and/or sell copies of", + "the Data Files or Software, and to permit persons to whom the Data Files", + "or Software are furnished to do so, provided that either", + "(a) this copyright and permission notice appear with all copies", + "of the Data Files or Software, or", + "(b) this copyright and permission notice appear in associated", + "Documentation.", + "", + "THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS.", + "IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS", + "NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL", + "DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,", + "DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER", + "TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR", + "PERFORMANCE OF THE DATA FILES OR SOFTWARE.", + "", + "Except as contained in this notice, the name of a copyright holder", + "shall not be used in advertising or otherwise to promote the sale,", + "use or other dealings in these Data Files or Software without prior", + "written authorization of the copyright holder.", + ] + }, + { + "name": "Document Object Model", + "license": "W3C License", + "description": "These files", + "licenseDetail": [ + "W3C License", + "This work is being provided by the copyright holders under the following license.", + "By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions.", + "Permission to copy, modify, and distribute this work, with or without modification,�for any purpose and without fee or royalty is hereby granted, provided that you include the following ", + "on ALL copies of the work or portions thereof, including modifications:", + "* The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.", + "* Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included.", + "* Notice of any changes or modifications, through a copyright statement on the new code or document such as \"This software or document includes material copied from or derived ", + "from [title and URI of the W3C document]. Copyright � [YEAR] W3C� (MIT, ERCIM, Keio, Beihang).\" ", + "Disclaimers", + "THIS WORK IS PROVIDED \"AS IS", + " AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR ", + "FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.", + "COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT.", + "The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. ", + "Title to copyright in this work will at all times remain with copyright holders.", + ] + }, + { + "name": "Web Background Synchronization", + "license": "W3C Community Final Specification Agreement", + "description": "TypeScript includes files related to this specification", + "licenseDetail": [ + "W3C Community Final Specification Agreement ", + "To secure commitments from participants for the full text of a Community or Business Group Report, the group may call for voluntary commitments to the following terms; a \"summary\" is ", + "available. See also the related \"W3C Community Contributor License Agreement\".", + "1. The Purpose of this Agreement.", + "This Agreement sets forth the terms under which I make certain copyright and patent rights available to you for your implementation of the Specification. ", + "Any other capitalized terms not specifically defined herein have the same meaning as those terms have in the \"W3C Patent Policy\", and if not defined there, in the \"W3C Process Document\".", + "2. Copyrights. ", + "2.1. Copyright Grant. I grant to you a perpetual (for the duration of the applicable copyright), worldwide, non-exclusive, no-charge, royalty-free, copyright license, without any obligation for accounting to me, to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, distribute, and implement the Specification to the full extent of my copyright interest in the Specification. ", + "2.2. Attribution. As a condition of the copyright grant, you must include an attribution to the Specification in any derivative work you make based on the Specification. That attribution must include, at minimum, the Specification name and version number.", + "3. Patents. ", + "3.1. Patent Licensing Commitment. I agree to license my Essential Claims under the W3C Community RF Licensing Requirements. This requirement includes Essential Claims that I own and any that I have the right to license without obligation of payment or other consideration to an unrelated third party. W3C Community RF Licensing Requirements obligations made concerning the Specification and described in this policy are binding on me for the life of the patents in question and encumber the patents containing Essential Claims, regardless of changes in participation status or W3C Membership. I also agree to license my Essential Claims under the W3C Community RF Licensing Requirements in derivative works of the Specification so long as all normative portions of the Specification are maintained and that this licensing commitment does not extend to any portion of the derivative work that was not included in the Specification.", + "3.2. Optional, Additional Patent Grant. In addition to the provisions of Section 3.1, I may also, at my option, make certain intellectual property rights infringed by implementations of the Specification, including Essential Claims, available by providing those terms via the W3C Web site.", + "4. No Other Rights. Except as specifically set forth in this Agreement, no other express or implied patent, trademark, copyright, or other property rights are granted under this Agreement, including by implication, waiver, or estoppel.", + "5. Antitrust Compliance. I acknowledge that I may compete with other participants, that I am under no obligation to implement the Specification, that each participant is free to develop competing technologies and standards, and that each party is free to license its patent rights to third parties, including for the purpose of enabling competing technologies and standards.", + "6. Non-Circumvention. I agree that I will not intentionally take or willfully assist any third party to take any action for the purpose of circumventing my obligations under this Agreement.", + "7. Transition to W3C Recommendation Track. The Specification developed by the Project may transition to the W3C Recommendation Track. The W3C Team is responsible for notifying me that a Corresponding Working Group has been chartered. I have no obligation to join the Corresponding Working Group. If the Specification developed by the Project transitions to the W3C Recommendation Track, the following terms apply: ", + "7.1. If I join the Corresponding Working Group. If I join the Corresponding Working Group, I will be subject to all W3C rules, obligations, licensing commitments, and policies that govern that Corresponding Working Group.", + "7.2. If I Do Not Join the Corresponding Working Group. ", + "7.2.1. Licensing Obligations to Resulting Specification. If I do not join the Corresponding Working Group, I agree to offer patent licenses according to the W3C Royalty-Free licensing requirements described in Section 5 of the W3C Patent Policy for the portions of the Specification included in the resulting Recommendation. This licensing commitment does not extend to any portion of an implementation of the Recommendation that was not included in the Specification. This licensing commitment may not be revoked but may be modified through the exclusion process defined in Section 4 of the W3C Patent Policy. I am not required to join the Corresponding Working Group to exclude patents from the W3C Royalty-Free licensing commitment, but must otherwise follow the normal exclusion procedures defined by the W3C Patent Policy. The W3C Team will notify me of any Call for Exclusion in the Corresponding Working Group as set forth in Section 4.5 of the W3C Patent Policy.", + "7.2.2. No Disclosure Obligation. If I do not join the Corresponding Working Group, I have no patent disclosure obligations outside of those set forth in Section 6 of the W3C Patent Policy.", + "8. Conflict of Interest. I will disclose significant relationships when those relationships might reasonably be perceived as creating a conflict of interest with my role. I will notify W3C of any change in my affiliation using W3C-provided mechanisms.", + "9. Representations, Warranties and Disclaimers. I represent and warrant that I am legally entitled to grant the rights and promises set forth in this Agreement. IN ALL OTHER RESPECTS THE SPECIFICATION IS PROVIDED �AS IS.� The entire risk as to implementing or otherwise using the Specification is assumed by the implementer and user. Except as stated herein, I expressly disclaim any warranties (express, implied, or otherwise), including implied warranties of merchantability, non-infringement, fitness for a particular purpose, or title, related to the Specification. IN NO EVENT WILL ANY PARTY BE LIABLE TO ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF ACTION OF ANY KIND WITH RESPECT TO THIS AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR NOT THE OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. All of my obligations under Section 3 regarding the transfer, successors in interest, or assignment of Granted Claims will be satisfied if I notify the transferee or assignee of any patent that I know contains Granted Claims of the obligations under Section 3. Nothing in this Agreement requires me to undertake a patent search.", + "10. Definitions. ", + "10.1. Agreement. �Agreement� means this W3C Community Final Specification Agreement.", + "10.2. Corresponding Working Group. �Corresponding Working Group� is a W3C Working Group that is chartered to develop a Recommendation, as defined in the W3C Process Document, that takes the Specification as an input.", + "10.3. Essential Claims. �Essential Claims� shall mean all claims in any patent or patent application in any jurisdiction in the world that would necessarily be infringed by implementation of the Specification. A claim is necessarily infringed hereunder only when it is not possible to avoid infringing it because there is no non-infringing alternative for implementing the normative portions of the Specification. Existence of a non-infringing alternative shall be judged based on the state of the art at the time of the publication of the Specification. The following are expressly excluded from and shall not be deemed to constitute Essential Claims: ", + "10.3.1. any claims other than as set forth above even if contained in the same patent as Essential Claims; and", + "10.3.2. claims which would be infringed only by: ", + "portions of an implementation that are not specified in the normative portions of the Specification, or", + "enabling technologies that may be necessary to make or use any product or portion thereof that complies with the Specification and are not themselves expressly set forth in the Specification (e.g., semiconductor manufacturing technology, compiler technology, object-oriented technology, basic operating system technology, and the like); or", + "the implementation of technology developed elsewhere and merely incorporated by reference in the body of the Specification.", + "10.3.3. design patents and design registrations.", + "For purposes of this definition, the normative portions of the Specification shall be deemed to include only architectural and interoperability requirements. Optional features in the RFC 2119 sense are considered normative unless they are specifically identified as informative. Implementation examples or any other material that merely illustrate the requirements of the Specification are informative, rather than normative.", + "10.4. I, Me, or My. �I,� �me,� or �my� refers to the signatory.", + "10.5 Project. �Project� means the W3C Community Group or Business Group for which I executed this Agreement.", + "10.6. Specification. �Specification� means the Specification identified by the Project as the target of this agreement in a call for Final Specification Commitments. W3C shall provide the authoritative mechanisms for the identification of this Specification.", + "10.7. W3C Community RF Licensing Requirements. �W3C Community RF Licensing Requirements� license shall mean a non-assignable, non-sublicensable license to make, have made, use, sell, have sold, offer to sell, import, and distribute and dispose of implementations of the Specification that: ", + "10.7.1. shall be available to all, worldwide, whether or not they are W3C Members;", + "10.7.2. shall extend to all Essential Claims owned or controlled by me;", + "10.7.3. may be limited to implementations of the Specification, and to what is required by the Specification;", + "10.7.4. may be conditioned on a grant of a reciprocal RF license (as defined in this policy) to all Essential Claims owned or controlled by the licensee. A reciprocal license may be required to be available to all, and a reciprocal license may itself be conditioned on a further reciprocal license from all.", + "10.7.5. may not be conditioned on payment of royalties, fees or other consideration;", + "10.7.6. may be suspended with respect to any licensee when licensor issued by licensee for infringement of claims essential to implement the Specification or any W3C Recommendation;", + "10.7.7. may not impose any further conditions or restrictions on the use of any technology, intellectual property rights, or other restrictions on behavior of the licensee, but may include reasonable, customary terms relating to operation or maintenance of the license relationship such as the following: choice of law and dispute resolution;", + "10.7.8. shall not be considered accepted by an implementer who manifests an intent not to accept the terms of the W3C Community RF Licensing Requirements license as offered by the licensor.", + "10.7.9. The RF license conforming to the requirements in this policy shall be made available by the licensor as long as the Specification is in effect. The term of such license shall be for the life of the patents in question.", + "I am encouraged to provide a contact from which licensing information can be obtained and other relevant licensing information. Any such information will be made publicly available. ", + "10.8. You or Your. �You,� �you,� or �your� means any person or entity who exercises copyright or patent rights granted under this Agreement, and any person that person or entity controls.", + ] + } +] \ No newline at end of file diff --git a/extensions/typescript-language-features/README.md b/extensions/typescript-language-features/README.md new file mode 100644 index 00000000000..26942f9d460 --- /dev/null +++ b/extensions/typescript-language-features/README.md @@ -0,0 +1,7 @@ +# Language Features for Typescript and Javascript files + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +See [Typescript in Visual Studio Code](https://code.visualstudio.com/docs/languages/typescript) and [Javascript in Visual Studio Code](https://code.visualstudio.com/docs/languages/javascript) to learn about the features of this extension. \ No newline at end of file diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 841024fd02b..6ad08cfc974 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -16,6 +16,7 @@ "Programming Languages" ], "dependencies": { + "jsonc-parser": "^2.0.1", "semver": "4.3.6", "vscode-extension-telemetry": "0.0.17", "vscode-nls": "^3.2.4" @@ -40,7 +41,11 @@ "onCommand:javascript.goToProjectConfig", "onCommand:typescript.goToProjectConfig", "onCommand:typescript.openTsServerLog", - "onCommand:workbench.action.tasks.runTask" + "onCommand:workbench.action.tasks.runTask", + "workspaceContains:**/tsconfig.json", + "workspaceContains:**/jsconfig.json", + "workspaceContains:**/tsconfig.*.json", + "workspaceContains:**/jsconfig.*.json" ], "main": "./out/extension", "contributes": { @@ -505,6 +510,16 @@ "default": "prompt", "description": "%typescript.updateImportsOnFileMove.enabled%", "scope": "resource" + }, + "typescript.autoClosingTags": { + "type": "boolean", + "default": true, + "description": "%typescript.autoClosingTags%" + }, + "javascript.autoClosingTags": { + "type": "boolean", + "default": true, + "description": "%typescript.autoClosingTags%" } } }, @@ -666,4 +681,4 @@ } ] } -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index b221d624d26..6876a3706ab 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -55,5 +55,6 @@ "typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: 'single' quotes, 'double' quotes, or 'auto' infer quote type from existing imports. Requires using TypeScript 2.9 or newer in the workspace.", "typescript.preferences.importModuleSpecifier": "Preferred path style for auto imports:\n- \"relative\" to the file location.\n- \"non-relative\" based on the 'baseUrl' configured in your 'jsconfig.json' / 'tsconfig.json'.\n- \"auto\" infer the shortest path type.\nRequires using TypeScript 2.9 or newer in the workspace.", "typescript.showUnused": "Enable/disable highlighting of unused variables in code. Requires using TypeScript 2.9 or newer in the workspace.", - "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Possible values are: 'prompt' on each rename, 'always' update paths automatically, and 'never' rename paths and don't prompt me. Requires using TypeScript 2.9 or newer in the workspace." + "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Possible values are: 'prompt' on each rename, 'always' update paths automatically, and 'never' rename paths and don't prompt me. Requires using TypeScript 2.9 or newer in the workspace.", + "typescript.autoClosingTags": "Enable/disable automatic closing of JSX tags. Requires using TypeScript 3.0 or newer in the workspace." } diff --git a/extensions/typescript-language-features/src/commands.ts b/extensions/typescript-language-features/src/commands.ts index 3dbad2d2560..fce4da698a2 100644 --- a/extensions/typescript-language-features/src/commands.ts +++ b/extensions/typescript-language-features/src/commands.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - +import * as nls from 'vscode-nls'; import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; import { Command } from './utils/commandManager'; import { Lazy } from './utils/lazy'; -import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './utils/tsconfig'; +import { isImplicitProjectConfigFile, openOrCreateConfigFile } from './utils/tsconfig'; + -import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index 521da08e6dc..74f0ac6f509 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -16,6 +16,7 @@ import LogDirectoryProvider from './utils/logDirectoryProvider'; import ManagedFileContextManager from './utils/managedFileContext'; import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from './utils/plugins'; import * as ProjectStatus from './utils/projectStatus'; +import { flatten } from './utils/arrays'; export function activate( @@ -32,7 +33,14 @@ export function activate( context.subscriptions.push(new TypeScriptTaskProviderManager(lazyClientHost.map(x => x.serviceClient))); context.subscriptions.push(new LanguageConfigurationManager()); - const supportedLanguage = [].concat.apply([], standardLanguageDescriptions.map(x => x.modeIds).concat(plugins.map(x => x.languages))); + import('./features/tsconfig').then(module => { + context.subscriptions.push(module.register()); + }); + + const supportedLanguage = flatten([ + ...standardLanguageDescriptions.map(x => x.modeIds), + ...plugins.map(x => x.languages) + ]); function didOpenTextDocument(textDocument: vscode.TextDocument): boolean { if (isSupportedDocument(supportedLanguage, textDocument)) { openListener.dispose(); diff --git a/extensions/typescript-language-features/src/features/baseCodeLensProvider.ts b/extensions/typescript-language-features/src/features/baseCodeLensProvider.ts index 1a2628f1e9b..9576008eab4 100644 --- a/extensions/typescript-language-features/src/features/baseCodeLensProvider.ts +++ b/extensions/typescript-language-features/src/features/baseCodeLensProvider.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Position, Event, EventEmitter } from 'vscode'; +import { CancellationToken, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, TextDocument, Uri } from 'vscode'; import * as Proto from '../protocol'; - import { ITypeScriptServiceClient } from '../typescriptService'; -import * as typeConverters from '../utils/typeConverters'; import { escapeRegExp } from '../utils/regexp'; +import * as typeConverters from '../utils/typeConverters'; + export class ReferencesCodeLens extends CodeLens { constructor( @@ -115,7 +115,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider } // TS 3.0+ provides a span for just the symbol - if ((item as any).nameSpan) { + if (item.nameSpan) { return typeConverters.Range.fromTextSpan((item as any).nameSpan); } diff --git a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts index b5560ee198e..f6b4e8c174e 100644 --- a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts @@ -11,17 +11,14 @@ import API from '../utils/api'; import { Delayer } from '../utils/async'; import { disposeAll } from '../utils/dispose'; import * as languageModeIds from '../utils/languageModeIds'; -import { ResourceMap } from './resourceMap'; +import { ResourceMap } from '../utils/resourceMap'; +import * as typeConverters from '../utils/typeConverters'; enum BufferKind { TypeScript = 1, JavaScript = 2, } -interface IDiagnosticRequestor { - requestDiagnostic(resource: Uri): void; -} - function mode2ScriptKind(mode: string): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined { switch (mode) { case languageModeIds.typescript: return 'TS'; @@ -37,7 +34,6 @@ class SyncedBuffer { constructor( private readonly document: TextDocument, public readonly filepath: string, - private readonly diagnosticRequestor: IDiagnosticRequestor, private readonly client: ITypeScriptServiceClient ) { } @@ -101,16 +97,11 @@ class SyncedBuffer { public onContentChanged(events: TextDocumentContentChangeEvent[]): void { for (const { range, text } of events) { const args: Proto.ChangeRequestArgs = { - file: this.filepath, - line: range.start.line + 1, - offset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1, - insertString: text + insertString: text, + ...typeConverters.Range.toFormattingRequestArgs(this.filepath, range) }; this.client.execute('change', args, false); } - this.diagnosticRequestor.requestDiagnostic(this.document.uri); } } @@ -123,9 +114,57 @@ class SyncedBufferMap extends ResourceMap { public get allBuffers(): Iterable { return this.values; } +} - public get allResources(): Iterable { - return this.keys; +class PendingDiagnostics extends ResourceMap { + public getFileList(): Set { + return new Set(Array.from(this.entries) + .sort((a, b) => a[1] - b[1]) + .map(entry => entry[0])); + } +} + +class GetErrRequest { + + public static executeGetErrRequest( + client: ITypeScriptServiceClient, + files: string[], + onDone: () => void + ) { + const token = new CancellationTokenSource(); + return new GetErrRequest(client, files, token, onDone); + } + + private _done: boolean = false; + + private constructor( + client: ITypeScriptServiceClient, + public readonly files: string[], + private readonly _token: CancellationTokenSource, + onDone: () => void + ) { + const args: Proto.GeterrRequestArgs = { + delay: 0, + files + }; + + client.executeAsync('geterr', args, _token.token) + .then(undefined, () => { }) + .then(() => { + if (this._done) { + return; + } + this._done = true; + onDone(); + }); + } + + public cancel(): any { + if (!this._done) { + this._token.cancel(); + } + + this._token.dispose(); } } @@ -138,10 +177,9 @@ export default class BufferSyncSupport { private readonly modeIds: Set; private readonly disposables: Disposable[] = []; private readonly syncedBuffers: SyncedBufferMap; - - private readonly pendingDiagnostics = new Map(); + private readonly pendingDiagnostics: PendingDiagnostics; private readonly diagnosticDelayer: Delayer; - private pendingGetErr: { request: Promise, files: string[], token: CancellationTokenSource } | undefined; + private pendingGetErr: GetErrRequest | undefined; private listening: boolean = false; constructor( @@ -153,10 +191,12 @@ export default class BufferSyncSupport { this.diagnosticDelayer = new Delayer(300); - this.syncedBuffers = new SyncedBufferMap(path => this.normalizePath(path)); + const pathNormalizer = (path: Uri) => this.client.normalizedPath(path); + this.syncedBuffers = new SyncedBufferMap(pathNormalizer); + this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer); this.updateConfiguration(); - workspace.onDidChangeConfiguration(() => this.updateConfiguration(), null); + workspace.onDidChangeConfiguration(this.updateConfiguration, this, this.disposables); } private readonly _onDelete = new EventEmitter(); @@ -210,10 +250,10 @@ export default class BufferSyncSupport { return; } - const syncedBuffer = new SyncedBuffer(document, filepath, this, this.client); + const syncedBuffer = new SyncedBuffer(document, filepath, this.client); this.syncedBuffers.set(resource, syncedBuffer); syncedBuffer.open(); - this.requestDiagnostic(resource); + this.requestDiagnostic(syncedBuffer); } public closeResource(resource: Uri): void { @@ -240,25 +280,23 @@ export default class BufferSyncSupport { } syncedBuffer.onContentChanged(e.contentChanges); - if (this.pendingGetErr) { - this.pendingGetErr.token.cancel(); - this.pendingGetErr = undefined; + const didTrigger = this.requestDiagnostic(syncedBuffer); - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + if (!didTrigger && this.pendingGetErr) { + // In this case we always want to re-trigger all diagnostics + this.pendingGetErr.cancel(); + this.pendingGetErr = undefined; + this.triggerDiagnostics(); } } public requestAllDiagnostics() { for (const buffer of this.syncedBuffers.allBuffers) { if (this.shouldValidate(buffer)) { - this.pendingDiagnostics.set(buffer.filepath, Date.now()); + this.pendingDiagnostics.set(buffer.resource, Date.now()); } } - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + this.triggerDiagnostics(); } public getErr(resources: Uri[]): any { @@ -268,80 +306,62 @@ export default class BufferSyncSupport { } for (const resource of handledResources) { - const file = this.client.normalizedPath(resource); - if (file) { - this.pendingDiagnostics.set(file, Date.now()); - } + this.pendingDiagnostics.set(resource, Date.now()); } - this.diagnosticDelayer.trigger(() => { - this.sendPendingDiagnostics(); - }, 200); + this.triggerDiagnostics(); } - public requestDiagnostic(resource: Uri): void { - const file = this.client.normalizedPath(resource); - if (!file) { - return; - } - - this.pendingDiagnostics.set(file, Date.now()); - const buffer = this.syncedBuffers.get(resource); - if (!buffer || !this.shouldValidate(buffer)) { - return; - } - - let delay = 300; - const lineCount = buffer.lineCount; - delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800); + private triggerDiagnostics(delay: number = 200) { this.diagnosticDelayer.trigger(() => { this.sendPendingDiagnostics(); }, delay); } + private requestDiagnostic(buffer: SyncedBuffer): boolean { + if (!this.shouldValidate(buffer)) { + return false; + } + + this.pendingDiagnostics.set(buffer.resource, Date.now()); + + const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800); + this.triggerDiagnostics(delay); + return true; + } + public hasPendingDiagnostics(resource: Uri): boolean { - const file = this.client.normalizedPath(resource); - return !file || this.pendingDiagnostics.has(file); + return this.pendingDiagnostics.has(resource); } private sendPendingDiagnostics(): void { - const files = new Set(Array.from(this.pendingDiagnostics.entries()) - .sort((a, b) => a[1] - b[1]) - .map(entry => entry[0])); + const fileList = this.pendingDiagnostics.getFileList(); // Add all open TS buffers to the geterr request. They might be visible - for (const file of this.syncedBuffers.allResources) { - if (!this.pendingDiagnostics.get(file)) { - files.add(file); + for (const buffer of this.syncedBuffers.values) { + if (!this.pendingDiagnostics.has(buffer.resource)) { + fileList.add(buffer.filepath); } } if (this.pendingGetErr) { for (const file of this.pendingGetErr.files) { - files.add(file); + fileList.add(file); } } - if (files.size) { - const fileList = Array.from(files); - const args: Proto.GeterrRequestArgs = { - delay: 0, - files: fileList - }; - const token = new CancellationTokenSource(); + if (fileList.size) { + if (this.pendingGetErr) { + this.pendingGetErr.cancel(); + } - const getErr = this.pendingGetErr = { - request: this.client.executeAsync('geterr', args, token.token) - .then(undefined, () => { }) - .then(() => { - if (this.pendingGetErr === getErr) { - this.pendingGetErr = undefined; - } - }), - files: fileList, - token - }; + const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, Array.from(fileList), () => { + if (this.pendingGetErr === getErr) { + this.pendingGetErr = undefined; + } + }); } + this.pendingDiagnostics.clear(); } @@ -363,8 +383,4 @@ export default class BufferSyncSupport { return this._validateTypeScript; } } - - private normalizePath(path: Uri): string | null { - return this.client.normalizedPath(path); - } } diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index c7ce7bf6e56..54e6661cac4 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -4,23 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - -import { ITypeScriptServiceClient } from '../typescriptService'; -import TypingsStatus from '../utils/typingsStatus'; - +import * as nls from 'vscode-nls'; import * as Proto from '../protocol'; import * as PConst from '../protocol.const'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; +import { applyCodeAction } from '../utils/codeAction'; +import { Command, CommandManager } from '../utils/commandManager'; import * as Previewer from '../utils/previewer'; import * as typeConverters from '../utils/typeConverters'; - -import * as nls from 'vscode-nls'; -import { applyCodeAction } from '../utils/codeAction'; -import { CommandManager, Command } from '../utils/commandManager'; +import TypingsStatus from '../utils/typingsStatus'; import FileConfigurationManager from './fileConfigurationManager'; -import API from '../utils/api'; +import { memoize } from '../utils/memoize'; const localize = nls.loadMessageBundle(); + +interface CommitCharactersSettings { + readonly enabled: boolean; + readonly enableDotCompletions: boolean; + readonly enableCallCompletions: boolean; +} + class MyCompletionItem extends vscode.CompletionItem { public readonly useCodeSnippet: boolean; @@ -29,10 +34,10 @@ class MyCompletionItem extends vscode.CompletionItem { public readonly document: vscode.TextDocument, line: string, public readonly tsEntry: Proto.CompletionEntry, - enableDotCompletions: boolean, - useCodeSnippetsOnMethodSuggest: boolean + useCodeSnippetsOnMethodSuggest: boolean, + public readonly commitCharactersSettings: CommitCharactersSettings ) { - super(tsEntry.name); + super(tsEntry.name, MyCompletionItem.convertKind(tsEntry.kind)); if (tsEntry.isRecommended) { // Make sure isRecommended property always comes first @@ -47,9 +52,7 @@ class MyCompletionItem extends vscode.CompletionItem { this.sortText = tsEntry.sortText; } - this.kind = MyCompletionItem.convertKind(tsEntry.kind); this.position = position; - this.commitCharacters = MyCompletionItem.getCommitCharacters(enableDotCompletions, !useCodeSnippetsOnMethodSuggest, tsEntry.kind); this.useCodeSnippet = useCodeSnippetsOnMethodSuggest && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method); if (tsEntry.replacementSpan) { this.range = typeConverters.Range.fromTextSpan(tsEntry.replacementSpan); @@ -81,19 +84,22 @@ class MyCompletionItem extends vscode.CompletionItem { } this.label += '?'; } + this.resolveRange(line); } - public resolve(): void { - if (!this.range) { - // Try getting longer, prefix based range for completions that span words - const wordRange = this.document.getWordRangeAtPosition(this.position); - const text = this.document.getText(new vscode.Range(this.position.line, Math.max(0, this.position.character - this.label.length), this.position.line, this.position.character)).toLowerCase(); - const entryName = this.label.toLowerCase(); - for (let i = entryName.length; i >= 0; --i) { - if (text.endsWith(entryName.substr(0, i)) && (!wordRange || wordRange.start.character > this.position.character - i)) { - this.range = new vscode.Range(this.position.line, Math.max(0, this.position.character - i), this.position.line, this.position.character); - break; - } + private resolveRange(line: string): void { + if (this.range) { + return; + } + + // Try getting longer, prefix based range for completions that span words + const wordRange = this.document.getWordRangeAtPosition(this.position); + const text = line.slice(Math.max(0, this.position.character - this.label.length), this.position.character).toLowerCase(); + const entryName = this.label.toLowerCase(); + for (let i = entryName.length; i >= 0; --i) { + if (text.endsWith(entryName.substr(0, i)) && (!wordRange || wordRange.start.character > this.position.character - i)) { + this.range = new vscode.Range(this.position.line, Math.max(0, this.position.character - i), this.position.line, this.position.character); + break; } } } @@ -132,7 +138,6 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.interface: return vscode.CompletionItemKind.Interface; case PConst.Kind.warning: - case PConst.Kind.file: case PConst.Kind.script: return vscode.CompletionItemKind.File; case PConst.Kind.directory: @@ -143,13 +148,14 @@ class MyCompletionItem extends vscode.CompletionItem { return vscode.CompletionItemKind.Property; } - private static getCommitCharacters( - enableDotCompletions: boolean, - enableCallCompletions: boolean, - kind: string - ): string[] | undefined { + @memoize + public get commitCharacters(): string[] | undefined { + if (!this.commitCharactersSettings.enabled) { + return undefined; + } + const commitCharacters: string[] = []; - switch (kind) { + switch (this.tsEntry.kind) { case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: case PConst.Kind.constructSignature: @@ -157,7 +163,7 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.indexSignature: case PConst.Kind.enum: case PConst.Kind.interface: - if (enableDotCompletions) { + if (this.commitCharactersSettings.enableDotCompletions) { commitCharacters.push('.'); } break; @@ -172,10 +178,10 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.class: case PConst.Kind.function: case PConst.Kind.memberFunction: - if (enableDotCompletions) { + if (this.commitCharactersSettings.enableDotCompletions) { commitCharacters.push('.', ','); } - if (enableCallCompletions) { + if (this.commitCharactersSettings.enableCallCompletions) { commitCharacters.push('('); } break; @@ -273,7 +279,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext - ): Promise { + ): Promise { if (this.typingsStatus.isAcquiringTypings) { return Promise.reject({ label: localize( @@ -287,14 +293,14 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider const file = this.client.toPath(document.uri); if (!file) { - return []; + return null; } const line = document.lineAt(position.line); const completionConfiguration = CompletionConfiguration.getConfigurationForResource(document.uri); if (!this.shouldTrigger(context, completionConfiguration, line, position)) { - return []; + return null; } await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); @@ -306,32 +312,36 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider triggerCharacter: context.triggerCharacter as Proto.CompletionsTriggerCharacter }; - let msg: Proto.CompletionEntry[] | undefined = undefined; + + let enableCommitCharacters = true; + let msg: ReadonlyArray | undefined = undefined; try { - const response = await this.client.execute('completions', args, token); - msg = response.body; - if (!msg) { - return []; + if (this.client.apiVersion.gte(API.v300)) { + const { body } = await this.client.execute('completionInfo', args, token); + if (!body) { + return null; + } + enableCommitCharacters = !body.isNewIdentifierLocation; + msg = body.entries; + } else { + const { body } = await this.client.execute('completions', args, token); + if (!body) { + return null; + } + msg = body; } } catch { - return []; + return null; } 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, completionConfiguration.useCodeSnippetsOnMethodSuggest, { + enabled: enableCommitCharacters, + enableDotCompletions, + enableCallCompletions: !completionConfiguration.useCodeSnippetsOnMethodSuggest + })); } public async resolveCompletionItem( @@ -347,8 +357,6 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider return undefined; } - item.resolve(); - const args: Proto.CompletionDetailsRequestArgs = { ...typeConverters.Position.toFileLocationRequestArgs(filepath, item.position), entryNames: [ @@ -356,14 +364,14 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider ] }; - let response: Proto.CompletionDetailsResponse; + let details: Proto.CompletionEntryDetails[] | undefined; try { - response = await this.client.execute('completionEntryDetails', args, token); + const response = await this.client.execute('completionEntryDetails', args, token); + details = response.body; } catch { return item; } - const details = response.body; if (!details || !details.length || !details[0]) { return item; } @@ -595,6 +603,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/features/definitionProviderBase.ts b/extensions/typescript-language-features/src/features/definitionProviderBase.ts index 88de5712748..8d958648799 100644 --- a/extensions/typescript-language-features/src/features/definitionProviderBase.ts +++ b/extensions/typescript-language-features/src/features/definitionProviderBase.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Position, CancellationToken, Location } from 'vscode'; - +import { CancellationToken, Location, Position, TextDocument } from 'vscode'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import * as typeConverters from '../utils/typeConverters'; + export default class TypeScriptDefinitionProviderBase { constructor( protected readonly client: ITypeScriptServiceClient diff --git a/extensions/typescript-language-features/src/features/definitions.ts b/extensions/typescript-language-features/src/features/definitions.ts index 9ff840cdab4..99b9c366247 100644 --- a/extensions/typescript-language-features/src/features/definitions.ts +++ b/extensions/typescript-language-features/src/features/definitions.ts @@ -17,12 +17,7 @@ export default class TypeScriptDefinitionProvider extends DefinitionProviderBase super(client); } - public async provideDefinition() { - // Implemented by provideDefinition2 - return undefined; - } - - public async provideDefinition2( + public async provideDefinition( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken | boolean @@ -44,10 +39,11 @@ export default class TypeScriptDefinitionProvider extends DefinitionProviderBase const span = response.body.textSpan ? typeConverters.Range.fromTextSpan(response.body.textSpan) : undefined; return locations .map(location => { - const loc = typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location); - return { - origin: span, - ...loc, + const target = typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location); + return { + originSelectionRange: span, + targetRange: target.range, + targetUri: target.uri, }; }); } catch { diff --git a/extensions/typescript-language-features/src/features/diagnostics.ts b/extensions/typescript-language-features/src/features/diagnostics.ts index aaf4becb8d8..5915247fe0c 100644 --- a/extensions/typescript-language-features/src/features/diagnostics.ts +++ b/extensions/typescript-language-features/src/features/diagnostics.ts @@ -3,27 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import { ResourceMap } from './resourceMap'; - -export class DiagnosticSet { - private _map = new ResourceMap(); - - public set( - file: vscode.Uri, - diagnostics: vscode.Diagnostic[] - ) { - this._map.set(file, diagnostics); - } - - public get(file: vscode.Uri): vscode.Diagnostic[] { - return this._map.get(file) || []; - } - - public clear(): void { - this._map = new ResourceMap(); - } -} +import * as vscode from 'vscode'; +import { ResourceMap } from '../utils/resourceMap'; +import { DiagnosticLanguage, allDiagnosticLangauges } from '../utils/languageDescription'; export enum DiagnosticKind { Syntax, @@ -31,25 +13,129 @@ export enum DiagnosticKind { Suggestion } -const allDiagnosticKinds = [DiagnosticKind.Syntax, DiagnosticKind.Semantic, DiagnosticKind.Suggestion]; +class FileDiagnostics { + private readonly _diagnostics = new Map(); + + constructor( + public readonly file: vscode.Uri, + public language: DiagnosticLanguage + ) { } + + public updateDiagnostics( + language: DiagnosticLanguage, + kind: DiagnosticKind, + diagnostics: vscode.Diagnostic[] + ): boolean { + if (language !== this.language) { + this._diagnostics.clear(); + this.language = language; + } + + if (diagnostics.length === 0) { + const existing = this._diagnostics.get(kind); + if (!existing || existing && existing.length === 0) { + // No need to update + return false; + } + } + + this._diagnostics.set(kind, diagnostics); + return true; + } + + public getDiagnostics(settings: DiagnosticSettings): vscode.Diagnostic[] { + if (!settings.getValidate(this.language)) { + return []; + } + + return [ + ...this.get(DiagnosticKind.Syntax), + ...this.get(DiagnosticKind.Semantic), + ...this.getSuggestionDiagnostics(settings), + ]; + } + + private getSuggestionDiagnostics(settings: DiagnosticSettings) { + const enableSuggestions = settings.getEnableSuggestions(this.language); + return this.get(DiagnosticKind.Suggestion).filter(x => { + if (!enableSuggestions) { + // Still show unused + return x.tags && x.tags.indexOf(vscode.DiagnosticTag.Unnecessary) !== -1; + } + return true; + }); + } + + private get(kind: DiagnosticKind): vscode.Diagnostic[] { + return this._diagnostics.get(kind) || []; + } +} + +interface LangaugeDiagnosticSettings { + readonly validate: boolean; + readonly enableSuggestions: boolean; +} + +class DiagnosticSettings { + private static readonly defaultSettings: LangaugeDiagnosticSettings = { + validate: true, + enableSuggestions: true + }; + + private readonly _languageSettings = new Map(); + + constructor() { + for (const language of allDiagnosticLangauges) { + this._languageSettings.set(language, DiagnosticSettings.defaultSettings); + } + } + + public getValidate(language: DiagnosticLanguage): boolean { + return this.get(language).validate; + } + + public setValidate(language: DiagnosticLanguage, value: boolean): boolean { + return this.update(language, settings => ({ + validate: value, + enableSuggestions: settings.enableSuggestions + })); + } + + public getEnableSuggestions(language: DiagnosticLanguage): boolean { + return this.get(language).enableSuggestions; + } + + public setEnableSuggestions(language: DiagnosticLanguage, value: boolean): boolean { + return this.update(language, settings => ({ + validate: settings.validate, + enableSuggestions: value + })); + } + + private get(language: DiagnosticLanguage): LangaugeDiagnosticSettings { + return this._languageSettings.get(language) || DiagnosticSettings.defaultSettings; + } + + private update(language: DiagnosticLanguage, f: (x: LangaugeDiagnosticSettings) => LangaugeDiagnosticSettings): boolean { + const currentSettings = this.get(language); + const newSettings = f(currentSettings); + this._languageSettings.set(language, newSettings); + return currentSettings.validate === newSettings.validate + && currentSettings.enableSuggestions && currentSettings.enableSuggestions; + } +} export class DiagnosticsManager { - - private readonly _diagnostics = new Map(); + private readonly _diagnostics = new ResourceMap(); + private readonly _settings = new DiagnosticSettings(); private readonly _currentDiagnostics: vscode.DiagnosticCollection; private _pendingUpdates = new ResourceMap(); - private _validate: boolean = true; - private _enableSuggestions: boolean = true; - private readonly updateDelay = 50; + private readonly _updateDelay = 50; constructor( owner: string ) { - for (const kind of allDiagnosticKinds) { - this._diagnostics.set(kind, new DiagnosticSet()); - } - this._currentDiagnostics = vscode.languages.createDiagnosticCollection(owner); } @@ -64,63 +150,56 @@ export class DiagnosticsManager { public reInitialize(): void { this._currentDiagnostics.clear(); + this._diagnostics.clear(); + } - for (const diagnosticSet of this._diagnostics.values()) { - diagnosticSet.clear(); + public setValidate(language: DiagnosticLanguage, value: boolean) { + const didUpdate = this._settings.setValidate(language, value); + if (didUpdate) { + this.rebuild(); } } - public set validate(value: boolean) { - if (this._validate === value) { - return; - } - - this._validate = value; - if (!value) { - this._currentDiagnostics.clear(); + public setEnableSuggestions(language: DiagnosticLanguage, value: boolean) { + const didUpdate = this._settings.setEnableSuggestions(language, value); + if (didUpdate) { + this.rebuild(); } } - public set enableSuggestions(value: boolean) { - if (this._enableSuggestions === value) { - return; - } - - this._enableSuggestions = value; - if (!value) { - this._currentDiagnostics.clear(); - } - } - - public diagnosticsReceived( + public updateDiagnostics( + file: vscode.Uri, + language: DiagnosticLanguage, kind: DiagnosticKind, + diagnostics: vscode.Diagnostic[] + ): void { + + let didUpdate = false; + const entry = this._diagnostics.get(file); + if (entry) { + didUpdate = entry.updateDiagnostics(language, kind, diagnostics); + } else if (diagnostics.length) { + const fileDiagnostics = new FileDiagnostics(file, language); + fileDiagnostics.updateDiagnostics(language, kind, diagnostics); + this._diagnostics.set(file, fileDiagnostics); + didUpdate = true; + } + + if (didUpdate) { + this.scheduleDiagnosticsUpdate(file); + } + } + + public configFileDiagnosticsReceived( file: vscode.Uri, diagnostics: vscode.Diagnostic[] ): void { - const collection = this._diagnostics.get(kind); - if (!collection) { - return; - } - - if (diagnostics.length === 0) { - const existing = collection.get(file); - if (existing.length === 0) { - // No need to update - return; - } - } - - collection.set(file, diagnostics); - - this.scheduleDiagnosticsUpdate(file); - } - - public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void { this._currentDiagnostics.set(file, diagnostics); } public delete(resource: vscode.Uri): void { this._currentDiagnostics.delete(resource); + this._diagnostics.delete(resource); } public getDiagnostics(file: vscode.Uri): vscode.Diagnostic[] { @@ -129,35 +208,24 @@ export class DiagnosticsManager { private scheduleDiagnosticsUpdate(file: vscode.Uri) { if (!this._pendingUpdates.has(file)) { - this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this.updateDelay)); + this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay)); } } - private updateCurrentDiagnostics(file: vscode.Uri) { + private updateCurrentDiagnostics(file: vscode.Uri): void { if (this._pendingUpdates.has(file)) { clearTimeout(this._pendingUpdates.get(file)); this._pendingUpdates.delete(file); } - if (!this._validate) { - return; - } - - const allDiagnostics = [ - ...this._diagnostics.get(DiagnosticKind.Syntax)!.get(file), - ...this._diagnostics.get(DiagnosticKind.Semantic)!.get(file), - ...this.getSuggestionDiagnostics(file), - ]; - this._currentDiagnostics.set(file, allDiagnostics); + const fileDiagnostics = this._diagnostics.get(file); + this._currentDiagnostics.set(file, fileDiagnostics ? fileDiagnostics.getDiagnostics(this._settings) : []); } - private getSuggestionDiagnostics(file: vscode.Uri) { - return this._diagnostics.get(DiagnosticKind.Suggestion)!.get(file).filter(x => { - if (!this._enableSuggestions) { - // Still show unused - return x.tags && x.tags.indexOf(vscode.DiagnosticTag.Unnecessary) !== -1; - } - return true; - }); + private rebuild(): void { + this._currentDiagnostics.clear(); + for (const fileDiagnostic of Array.from(this._diagnostics.values)) { + this._currentDiagnostics.set(fileDiagnostic.file, fileDiagnostic.getDiagnostics(this._settings)); + } } } \ No newline at end of file diff --git a/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts b/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts index 41d5bd8e8cd..09a6fcf8c56 100644 --- a/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts +++ b/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts @@ -6,8 +6,8 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { ITypeScriptServiceClient } from '../typescriptService'; -import { VersionDependentRegistration } from '../utils/dependentRegistration'; import API from '../utils/api'; +import { VersionDependentRegistration } from '../utils/dependentRegistration'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/features/documentHighlight.ts b/extensions/typescript-language-features/src/features/documentHighlight.ts index 2e7f619739f..dd0fb76f735 100644 --- a/extensions/typescript-language-features/src/features/documentHighlight.ts +++ b/extensions/typescript-language-features/src/features/documentHighlight.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - import * as Proto from '../protocol'; - import { ITypeScriptServiceClient } from '../typescriptService'; import * as typeConverters from '../utils/typeConverters'; + + class TypeScriptDocumentHighlightProvider implements vscode.DocumentHighlightProvider { public constructor( private readonly client: ITypeScriptServiceClient @@ -26,18 +26,21 @@ class TypeScriptDocumentHighlightProvider implements vscode.DocumentHighlightPro } const args = typeConverters.Position.toFileLocationRequestArgs(file, position); + let items: Proto.OccurrencesResponseItem[] | undefined; try { const response = await this.client.execute('occurrences', args, token); - if (response && response.body) { - return response.body - .filter(x => !x.isInString) - .map(documentHighlightFromOccurance); - } + items = response.body; } catch { // noop } - return []; + if (!items) { + return []; + } + + return items + .filter(x => !x.isInString) + .map(documentHighlightFromOccurance); } } diff --git a/extensions/typescript-language-features/src/features/documentSymbol.ts b/extensions/typescript-language-features/src/features/documentSymbol.ts index 476ecf8916d..84fc9117b9a 100644 --- a/extensions/typescript-language-features/src/features/documentSymbol.ts +++ b/extensions/typescript-language-features/src/features/documentSymbol.ts @@ -4,12 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - import * as Proto from '../protocol'; import * as PConst from '../protocol.const'; import { ITypeScriptServiceClient } from '../typescriptService'; import * as typeConverters from '../utils/typeConverters'; -import API from '../utils/api'; const getSymbolKind = (kind: string): vscode.SymbolKind => { switch (kind) { @@ -33,60 +31,33 @@ const getSymbolKind = (kind: string): vscode.SymbolKind => { class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider { public constructor( - private readonly client: ITypeScriptServiceClient) { } + private readonly client: ITypeScriptServiceClient + ) { } - public async provideDocumentSymbols(resource: vscode.TextDocument, token: vscode.CancellationToken): Promise { - const filepath = this.client.toPath(resource.uri); - if (!filepath) { - return []; + public async provideDocumentSymbols(resource: vscode.TextDocument, token: vscode.CancellationToken): Promise { + const file = this.client.toPath(resource.uri); + if (!file) { + return undefined; } - const args: Proto.FileRequestArgs = { - file: filepath - }; + + let tree: Proto.NavigationTree | undefined; try { - if (this.client.apiVersion.gte(API.v206)) { - const response = await this.client.execute('navtree', args, token); - if (response.body) { - // The root represents the file. Ignore this when showing in the UI - const tree = response.body; - if (tree.childItems) { - const result = new Array(); - tree.childItems.forEach(item => TypeScriptDocumentSymbolProvider.convertNavTree(resource.uri, result, item)); - return result; - } - } - } else { - const response = await this.client.execute('navbar', args, token); - if (response.body) { - const result = new Array(); - const foldingMap: ObjectMap = Object.create(null); - response.body.forEach(item => TypeScriptDocumentSymbolProvider.convertNavBar(resource.uri, 0, foldingMap, result as vscode.SymbolInformation[], item)); - return result; - } - } - return []; - } catch (e) { - return []; + const args: Proto.FileRequestArgs = { file }; + const response = await this.client.execute('navtree', args, token); + tree = response.body; + } catch { + return undefined; } - } - private static convertNavBar(resource: vscode.Uri, indent: number, foldingMap: ObjectMap, bucket: vscode.SymbolInformation[], item: Proto.NavigationBarItem, containerLabel?: string): void { - const realIndent = indent + item.indent; - const key = `${realIndent}|${item.text}`; - if (realIndent !== 0 && !foldingMap[key] && TypeScriptDocumentSymbolProvider.shouldInclueEntry(item)) { - const result = new vscode.SymbolInformation(item.text, - getSymbolKind(item.kind), - containerLabel ? containerLabel : '', - typeConverters.Location.fromTextSpan(resource, item.spans[0])); - foldingMap[key] = result; - bucket.push(result); - } - if (item.childItems && item.childItems.length > 0) { - for (const child of item.childItems) { - TypeScriptDocumentSymbolProvider.convertNavBar(resource, realIndent + 1, foldingMap, bucket, child, item.text); - } + if (tree && tree.childItems) { + // The root represents the file. Ignore this when showing in the UI + const result: vscode.DocumentSymbol[] = []; + tree.childItems.forEach(item => TypeScriptDocumentSymbolProvider.convertNavTree(resource.uri, result, item)); + return result; } + + return undefined; } private static convertNavTree(resource: vscode.Uri, bucket: vscode.DocumentSymbol[], item: Proto.NavigationTree): boolean { diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts index 0cb8c3da539..1869ae9ae1b 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace as Workspace, FormattingOptions, TextDocument, CancellationToken, window, Disposable, workspace, WorkspaceConfiguration } from 'vscode'; - +import { CancellationToken, Disposable, FormattingOptions, TextDocument, window, workspace as Workspace, workspace, WorkspaceConfiguration } from 'vscode'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import * as languageIds from '../utils/languageModeIds'; import API from '../utils/api'; +import { isTypeScriptDocument } from '../utils/languageModeIds'; + function objsAreEqual(a: T, b: T): boolean { let keys = Object.keys(a); @@ -89,12 +89,12 @@ export default class FileConfigurationManager { return; } + this.formatOptions[key] = currentOptions; const args: Proto.ConfigureRequestArguments = { file, ...currentOptions, }; await this.client.execute('configure', args, token); - this.formatOptions[key] = currentOptions; } public reset() { @@ -175,8 +175,4 @@ function getImportModuleSpecifierPreference(config: WorkspaceConfiguration) { case 'non-relative': return 'non-relative'; default: return undefined; } -} - -function isTypeScriptDocument(document: TextDocument) { - return document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact; -} +} \ No newline at end of file diff --git a/extensions/typescript-language-features/src/features/folding.ts b/extensions/typescript-language-features/src/features/folding.ts index 556fafcbaa9..78f3193fe00 100644 --- a/extensions/typescript-language-features/src/features/folding.ts +++ b/extensions/typescript-language-features/src/features/folding.ts @@ -6,9 +6,9 @@ import * as vscode from 'vscode'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import * as typeConverters from '../utils/typeConverters'; -import { VersionDependentRegistration } from '../utils/dependentRegistration'; import API from '../utils/api'; +import { VersionDependentRegistration } from '../utils/dependentRegistration'; +import * as typeConverters from '../utils/typeConverters'; class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider { public constructor( diff --git a/extensions/typescript-language-features/src/features/formatting.ts b/extensions/typescript-language-features/src/features/formatting.ts index e0556c99ec8..89f68500c5b 100644 --- a/extensions/typescript-language-features/src/features/formatting.ts +++ b/extensions/typescript-language-features/src/features/formatting.ts @@ -10,59 +10,35 @@ import { ConfigurationDependentRegistration } from '../utils/dependentRegistrati import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; - class TypeScriptFormattingProvider implements vscode.DocumentRangeFormattingEditProvider, vscode.OnTypeFormattingEditProvider { - private enabled: boolean = true; - public constructor( private readonly client: ITypeScriptServiceClient, private readonly formattingOptionsManager: FileConfigurationManager ) { } - public updateConfiguration(config: vscode.WorkspaceConfiguration): void { - this.enabled = config.get('format.enable', true); - } - - public isEnabled(): boolean { - return this.enabled; - } - - private async doFormat( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - args: Proto.FormatRequestArgs, - token: vscode.CancellationToken - ): Promise { - await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token); - try { - const response = await this.client.execute('format', args, token); - if (response.body) { - return response.body.map(typeConverters.TextEdit.fromCodeEdit); - } - } catch { - // noop - } - return []; - } - public async provideDocumentRangeFormattingEdits( document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions, token: vscode.CancellationToken - ): Promise { - const absPath = this.client.toPath(document.uri); - if (!absPath) { - return []; + ): Promise { + const file = this.client.toPath(document.uri); + if (!file) { + return undefined; } - const args: Proto.FormatRequestArgs = { - file: absPath, - line: range.start.line + 1, - offset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 - }; - return this.doFormat(document, options, args, token); + + await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token); + + let edits: Proto.CodeEdit[] | undefined; + try { + const args = typeConverters.Range.toFormattingRequestArgs(file, range); + const response = await this.client.execute('format', args, token); + edits = response.body; + } catch { + // noop + } + + return (edits || []).map(typeConverters.TextEdit.fromCodeEdit); } public async provideOnTypeFormattingEdits( @@ -72,17 +48,15 @@ class TypeScriptFormattingProvider implements vscode.DocumentRangeFormattingEdit options: vscode.FormattingOptions, token: vscode.CancellationToken ): Promise { - const filepath = this.client.toPath(document.uri); - if (!filepath) { + const file = this.client.toPath(document.uri); + if (!file) { return []; } await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token); const args: Proto.FormatOnKeyRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1, + ...typeConverters.Position.toFileLocationRequestArgs(file, position), key: ch }; try { diff --git a/extensions/typescript-language-features/src/features/hover.ts b/extensions/typescript-language-features/src/features/hover.ts index e6fce8635c1..fcc2b3401a8 100644 --- a/extensions/typescript-language-features/src/features/hover.ts +++ b/extensions/typescript-language-features/src/features/hover.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import { tagsMarkdownPreview } from '../utils/previewer'; import * as typeConverters from '../utils/typeConverters'; + class TypeScriptHoverProvider implements vscode.HoverProvider { public constructor( diff --git a/extensions/typescript-language-features/src/features/organizeImports.ts b/extensions/typescript-language-features/src/features/organizeImports.ts index 944fcf7c585..ecf080b313f 100644 --- a/extensions/typescript-language-features/src/features/organizeImports.ts +++ b/extensions/typescript-language-features/src/features/organizeImports.ts @@ -7,11 +7,12 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; import { Command, CommandManager } from '../utils/commandManager'; +import { VersionDependentRegistration } from '../utils/dependentRegistration'; import * as typeconverts from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; -import { VersionDependentRegistration } from '../utils/dependentRegistration'; -import API from '../utils/api'; +import TelemetryReporter from '../utils/telemetry'; const localize = nls.loadMessageBundle(); @@ -22,10 +23,20 @@ class OrganizeImportsCommand implements Command { public readonly id = OrganizeImportsCommand.Id; constructor( - private readonly client: ITypeScriptServiceClient + private readonly client: ITypeScriptServiceClient, + private readonly telemetryReporter: TelemetryReporter, ) { } public async execute(file: string): Promise { + /* __GDPR__ + "organizeImports.execute" : { + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.telemetryReporter.logTelemetry('organizeImports.execute', {}); + const args: Proto.OrganizeImportsRequestArgs = { scope: { type: 'file', @@ -40,7 +51,7 @@ class OrganizeImportsCommand implements Command { } const edits = typeconverts.WorkspaceEdit.fromFileCodeEdits(this.client, response.body); - return await vscode.workspace.applyEdit(edits); + return vscode.workspace.applyEdit(edits); } } @@ -49,8 +60,10 @@ export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvi private readonly client: ITypeScriptServiceClient, commandManager: CommandManager, private readonly fileConfigManager: FileConfigurationManager, + telemetryReporter: TelemetryReporter, + ) { - commandManager.register(new OrganizeImportsCommand(client)); + commandManager.register(new OrganizeImportsCommand(client, telemetryReporter)); } public readonly metadata: vscode.CodeActionProviderMetadata = { @@ -82,10 +95,11 @@ export function register( selector: vscode.DocumentSelector, client: ITypeScriptServiceClient, commandManager: CommandManager, - fileConfigurationManager: FileConfigurationManager + fileConfigurationManager: FileConfigurationManager, + telemetryReporter: TelemetryReporter, ) { return new VersionDependentRegistration(client, API.v280, () => { - const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager); + const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager, telemetryReporter); return vscode.languages.registerCodeActionsProvider(selector, organizeImportsProvider, organizeImportsProvider.metadata); diff --git a/extensions/typescript-language-features/src/features/refactor.ts b/extensions/typescript-language-features/src/features/refactor.ts index c4bc4b58116..fbb3a3b88ef 100644 --- a/extensions/typescript-language-features/src/features/refactor.ts +++ b/extensions/typescript-language-features/src/features/refactor.ts @@ -9,9 +9,9 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { Command, CommandManager } from '../utils/commandManager'; import { VersionDependentRegistration } from '../utils/dependentRegistration'; +import TelemetryReporter from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; import FormattingOptionsManager from './fileConfigurationManager'; -import TelemetryReporter from '../utils/telemetry'; class ApplyRefactoringCommand implements Command { @@ -140,17 +140,18 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { await this.formattingOptionsManager.ensureConfigurationForDocument(document, undefined); const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs(file, rangeOrSelection); - let response: Proto.GetApplicableRefactorsResponse; + let refactorings: Proto.ApplicableRefactorInfo[]; try { - response = await this.client.execute('getApplicableRefactors', args, token); - if (!response || !response.body) { + const response = await this.client.execute('getApplicableRefactors', args, token); + if (!response.body) { return undefined; } + refactorings = response.body; } catch { return undefined; } - return this.convertApplicableRefactors(response.body, document, file, rangeOrSelection); + return this.convertApplicableRefactors(refactorings, document, file, rangeOrSelection); } private convertApplicableRefactors( diff --git a/extensions/typescript-language-features/src/features/references.ts b/extensions/typescript-language-features/src/features/references.ts index 4664c1b1ce3..a4b81e54f5b 100644 --- a/extensions/typescript-language-features/src/features/references.ts +++ b/extensions/typescript-language-features/src/features/references.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - import { ITypeScriptServiceClient } from '../typescriptService'; -import * as typeConverters from '../utils/typeConverters'; import API from '../utils/api'; +import * as typeConverters from '../utils/typeConverters'; + class TypeScriptReferenceSupport implements vscode.ReferenceProvider { public constructor( diff --git a/extensions/typescript-language-features/src/features/rename.ts b/extensions/typescript-language-features/src/features/rename.ts index 576f7fa62f0..c73539409fa 100644 --- a/extensions/typescript-language-features/src/features/rename.ts +++ b/extensions/typescript-language-features/src/features/rename.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; - import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import * as typeConverters from '../utils/typeConverters'; + class TypeScriptRenameProvider implements vscode.RenameProvider { public constructor( private readonly client: ITypeScriptServiceClient diff --git a/extensions/typescript-language-features/src/features/tagClosing.ts b/extensions/typescript-language-features/src/features/tagClosing.ts new file mode 100644 index 00000000000..66af4920ef9 --- /dev/null +++ b/extensions/typescript-language-features/src/features/tagClosing.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as Proto from '../protocol'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; +import { ConditionalRegistration, ConfigurationDependentRegistration, VersionDependentRegistration } from '../utils/dependentRegistration'; +import { disposeAll } from '../utils/dispose'; +import * as typeConverters from '../utils/typeConverters'; + +class TagClosing { + + private _disposed = false; + private _timeout: NodeJS.Timer | undefined = undefined; + private _cancel: vscode.CancellationTokenSource | undefined = undefined; + private readonly _disposables: vscode.Disposable[] = []; + + constructor( + private readonly client: ITypeScriptServiceClient + ) { + vscode.workspace.onDidChangeTextDocument( + event => this.onDidChangeTextDocument(event.document, event.contentChanges), + null, + this._disposables); + } + + public dispose() { + this._disposed = true; + + disposeAll(this._disposables); + + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = undefined; + } + + if (this._cancel) { + this._cancel.cancel(); + this._cancel.dispose(); + this._cancel = undefined; + } + } + + private onDidChangeTextDocument( + document: vscode.TextDocument, + changes: vscode.TextDocumentContentChangeEvent[] + ) { + const activeDocument = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document; + if (document !== activeDocument || changes.length === 0) { + return; + } + + const filepath = this.client.toPath(document.uri); + if (!filepath) { + return; + } + + if (typeof this._timeout !== 'undefined') { + clearTimeout(this._timeout); + } + + if (this._cancel) { + this._cancel.cancel(); + this._cancel.dispose(); + this._cancel = undefined; + } + + const lastChange = changes[changes.length - 1]; + const lastCharacter = lastChange.text[lastChange.text.length - 1]; + if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') { + return; + } + + const secondToLastCharacter = lastChange.text[lastChange.text.length - 2]; + if (secondToLastCharacter === '>') { + return; + } + + const rangeStart = lastChange.range.start; + const version = document.version; + this._timeout = setTimeout(async () => { + this._timeout = undefined; + + if (this._disposed) { + return; + } + + let position = new vscode.Position(rangeStart.line, rangeStart.character + lastChange.text.length); + let body: Proto.TextInsertion | undefined = undefined; + const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); + + this._cancel = new vscode.CancellationTokenSource(); + try { + const response = await this.client.execute('jsxClosingTag', args, this._cancel.token); + body = response && response.body; + if (!body) { + return; + } + } catch { + return; + } + + if (this._disposed) { + return; + } + + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + const activeDocument = activeEditor.document; + if (document === activeDocument && activeDocument.version === version) { + activeEditor.insertSnippet( + this.getTagSnippet(body), + this.getInsertionPositions(activeEditor, position)); + } + }, 100); + } + + private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString { + const snippet = new vscode.SnippetString(); + snippet.appendPlaceholder('', 0); + snippet.appendText(closingTag.newText); + return snippet; + } + + private getInsertionPositions(editor: vscode.TextEditor, position: vscode.Position) { + const activeSelectionPositions = editor.selections.map(s => s.active); + return activeSelectionPositions.some(p => p.isEqual(position)) + ? activeSelectionPositions + : position; + } +} + +export class ActiveDocumentDependentRegistration { + private readonly _registration: ConditionalRegistration; + private readonly _disposables: vscode.Disposable[] = []; + + constructor( + private readonly selector: vscode.DocumentSelector, + register: () => vscode.Disposable, + ) { + this._registration = new ConditionalRegistration(register); + vscode.window.onDidChangeActiveTextEditor(this.update, this, this._disposables); + this.update(); + } + + public dispose() { + disposeAll(this._disposables); + this._registration.dispose(); + } + + private update() { + const editor = vscode.window.activeTextEditor; + const enabled = !!(editor && vscode.languages.match(this.selector, editor.document)); + this._registration.update(enabled); + } +} + +export function register( + selector: vscode.DocumentSelector, + modeId: string, + client: ITypeScriptServiceClient, +) { + return new VersionDependentRegistration(client, API.v300, () => + new ConfigurationDependentRegistration(modeId, 'autoClosingTags', () => + new ActiveDocumentDependentRegistration(selector, () => + new TagClosing(client)))); +} diff --git a/extensions/typescript-language-features/src/features/tagCompletion.ts b/extensions/typescript-language-features/src/features/tagCompletion.ts deleted file mode 100644 index 8a8495180ca..00000000000 --- a/extensions/typescript-language-features/src/features/tagCompletion.ts +++ /dev/null @@ -1,66 +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 * as vscode from 'vscode'; -import * as Proto from '../protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import API from '../utils/api'; -import { VersionDependentRegistration } from '../utils/dependentRegistration'; -import * as typeConverters from '../utils/typeConverters'; - -class TypeScriptTagCompletion implements vscode.CompletionItemProvider { - constructor( - private readonly client: ITypeScriptServiceClient - ) { } - - async provideCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - token: vscode.CancellationToken, - _context: vscode.CompletionContext - ): Promise { - const filepath = this.client.toPath(document.uri); - if (!filepath) { - return undefined; - } - - const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); - let body: Proto.TextInsertion | undefined = undefined; - try { - const response = await this.client.execute('jsxClosingTag', args, token); - body = response && response.body; - if (!body) { - return undefined; - } - } catch { - return undefined; - } - - return [this.getCompletion(body)]; - } - - private getCompletion(body: Proto.TextInsertion) { - const completion = new vscode.CompletionItem(body.newText); - completion.insertText = this.getTagSnippet(body); - return completion; - } - - private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString { - const snippet = new vscode.SnippetString(); - snippet.appendPlaceholder('', 0); - snippet.appendText(closingTag.newText); - return snippet; - } -} - -export function register( - selector: vscode.DocumentSelector, - client: ITypeScriptServiceClient, -) { - return new VersionDependentRegistration(client, API.v300, () => - vscode.languages.registerCompletionItemProvider(selector, - new TypeScriptTagCompletion(client), - '>')); -} diff --git a/extensions/typescript-language-features/src/features/task.ts b/extensions/typescript-language-features/src/features/task.ts index c0d7fb00511..e819a1c092b 100644 --- a/extensions/typescript-language-features/src/features/task.ts +++ b/extensions/typescript-language-features/src/features/task.ts @@ -8,14 +8,14 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; - +import * as nls from 'vscode-nls'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider'; -import { isImplicitProjectConfigFile } from '../utils/tsconfig'; - -import * as nls from 'vscode-nls'; import { Lazy } from '../utils/lazy'; +import { isImplicitProjectConfigFile } from '../utils/tsconfig'; +import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider'; + + const localize = nls.loadMessageBundle(); type AutoDetect = 'on' | 'off' | 'build' | 'watch'; @@ -78,7 +78,10 @@ class TscTaskProvider implements vscode.TaskProvider { private async getAllTsConfigs(token: vscode.CancellationToken): Promise { const out = new Set(); - const configs = (await this.getTsConfigForActiveFile(token)).concat(await this.getTsConfigsInWorkspace()); + const configs = [ + ...await this.getTsConfigForActiveFile(token), + ...await this.getTsConfigsInWorkspace() + ]; for (const config of configs) { if (await exists(config.path)) { out.add(config); diff --git a/extensions/typescript-language-features/src/features/tsconfig.ts b/extensions/typescript-language-features/src/features/tsconfig.ts new file mode 100644 index 00000000000..414c0f01148 --- /dev/null +++ b/extensions/typescript-language-features/src/features/tsconfig.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as jsonc from 'jsonc-parser'; +import { dirname, join } from 'path'; +import * as vscode from 'vscode'; +import { flatten } from '../utils/arrays'; + +function mapNode(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] { + return node && node.type === 'array' && node.children + ? node.children.map(f) + : []; +} + +class TsconfigLinkProvider implements vscode.DocumentLinkProvider { + + public provideDocumentLinks( + document: vscode.TextDocument, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + const root = jsonc.parseTree(document.getText()); + if (!root) { + return null; + } + + return [ + this.getExendsLink(document, root), + ...this.getFilesLinks(document, root), + ...this.getReferencesLinks(document, root) + ].filter(x => !!x) as vscode.DocumentLink[]; + } + + private getExendsLink(document: vscode.TextDocument, root: jsonc.Node): vscode.DocumentLink | undefined { + return this.pathNodeToLink(document, jsonc.findNodeAtLocation(root, ['extends'])); + } + + private getFilesLinks(document: vscode.TextDocument, root: jsonc.Node) { + return mapNode( + jsonc.findNodeAtLocation(root, ['files']), + node => this.pathNodeToLink(document, node)); + } + + private getReferencesLinks(document: vscode.TextDocument, root: jsonc.Node) { + return mapNode( + jsonc.findNodeAtLocation(root, ['references']), + child => this.pathNodeToLink(document, jsonc.findNodeAtLocation(child, ['path']))); + } + + private pathNodeToLink( + document: vscode.TextDocument, + node: jsonc.Node | undefined + ): vscode.DocumentLink | undefined { + return this.isPathValue(node) + ? new vscode.DocumentLink(this.getRange(document, node), this.getTarget(document, node)) + : undefined; + } + + private isPathValue(extendsNode: jsonc.Node | undefined): extendsNode is jsonc.Node { + return extendsNode + && extendsNode.type === 'string' + && extendsNode.value + && !(extendsNode.value as string).includes('*'); + } + + private getTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri { + return vscode.Uri.file(join(dirname(document.uri.fsPath), node!.value)); + } + + private getRange(document: vscode.TextDocument, node: jsonc.Node) { + const offset = node!.offset; + const start = document.positionAt(offset + 1); + const end = document.positionAt(offset + (node!.length - 1)); + return new vscode.Range(start, end); + } +} + +export function register() { + const patterns: vscode.GlobPattern[] = [ + '**/[jt]sconfig.json', + '**/[jt]sconfig.*.json', + ]; + + const languages = ['json', 'jsonc']; + + const selector: vscode.DocumentSelector = flatten( + languages.map(language => + patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern })))); + + return vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider()); +} diff --git a/extensions/typescript-language-features/src/features/typeDefinitions.ts b/extensions/typescript-language-features/src/features/typeDefinitions.ts index 9ec9b48222e..23f8b18af39 100644 --- a/extensions/typescript-language-features/src/features/typeDefinitions.ts +++ b/extensions/typescript-language-features/src/features/typeDefinitions.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; import { VersionDependentRegistration } from '../utils/dependentRegistration'; import DefinitionProviderBase from './definitionProviderBase'; -import API from '../utils/api'; export default class TypeScriptTypeDefinitionProvider extends DefinitionProviderBase implements vscode.TypeDefinitionProvider { public provideTypeDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken | boolean): Promise { diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index 6f2d1265275..9b2f3c3ce5f 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -10,11 +10,11 @@ import * as nls from 'vscode-nls'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; -import * as languageIds from '../utils/languageModeIds'; +import * as fileSchemes from '../utils/fileSchemes'; +import { isTypeScriptDocument } from '../utils/languageModeIds'; +import { escapeRegExp } from '../utils/regexp'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; -import * as fileSchemes from '../utils/fileSchemes'; -import { escapeRegExp } from '../utils/regexp'; const localize = nls.loadMessageBundle(); @@ -83,14 +83,20 @@ export class UpdateImportsOnFileRenameHandler { this.client.bufferSyncSupport.closeResource(targetResource); this.client.bufferSyncSupport.openTextDocument(document); - // Workaround for https://github.com/Microsoft/vscode/issues/52967 - // Never attempt to update import paths if the file does not contain something the looks like an export - const tree = await this.client.execute('navtree', { file: newFile }); - const hasExport = (node: Proto.NavigationTree): boolean => { - return !!node.kindModifiers.match(/\bexport\b/g) || !!(node.childItems && node.childItems.some(hasExport)); - }; - if (!tree.body || !tree.body || !hasExport(tree.body)) { - return; + if (!this.client.apiVersion.gte(API.v300) && !fs.lstatSync(newResource.fsPath).isDirectory()) { + // Workaround for https://github.com/Microsoft/vscode/issues/52967 + // Never attempt to update import paths if the file does not contain something the looks like an export + try { + const tree = await this.client.execute('navtree', { file: newFile }); + const hasExport = (node: Proto.NavigationTree): boolean => { + return !!node.kindModifiers.match(/\bexports?\b/g) || !!(node.childItems && node.childItems.some(hasExport)); + }; + if (!tree.body || !tree.body || !hasExport(tree.body)) { + return; + } + } catch { + // noop + } } const edits = await this.getEditsForFileRename(targetFile, document, oldFile, newFile); @@ -203,7 +209,12 @@ export class UpdateImportsOnFileRenameHandler { return undefined; } - if (this.client.apiVersion.gte(API.v292) && fs.lstatSync(resource.fsPath).isDirectory()) { + const isDirectory = fs.lstatSync(resource.fsPath).isDirectory(); + if (isDirectory && this.client.apiVersion.gte(API.v300)) { + return resource; + } + + if (isDirectory && this.client.apiVersion.gte(API.v292)) { const files = await vscode.workspace.findFiles({ base: resource.fsPath, pattern: '**/*.{ts,tsx,js,jsx}', @@ -223,7 +234,7 @@ export class UpdateImportsOnFileRenameHandler { const isDirectoryRename = fs.lstatSync(newFile).isDirectory(); await this.fileConfigurationManager.ensureConfigurationForDocument(document, undefined); - const args: Proto.GetEditsForFileRenameRequestArgs = { + const args: Proto.GetEditsForFileRenameRequestArgs & { file: string } = { file: targetResource, oldFilePath: oldFile, newFilePath: newFile, @@ -293,6 +304,3 @@ export class UpdateImportsOnFileRenameHandler { } } -function isTypeScriptDocument(document: vscode.TextDocument) { - return document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact; -} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 2d7cde5e693..31ba616b8c1 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -3,30 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { basename } from 'path'; - -import TypeScriptServiceClient from './typescriptServiceClient'; -import TypingsStatus from './utils/typingsStatus'; -import FileConfigurationManager from './features/fileConfigurationManager'; -import { CommandManager } from './utils/commandManager'; -import { DiagnosticsManager, DiagnosticKind } from './features/diagnostics'; -import { LanguageDescription } from './utils/languageDescription'; -import * as fileSchemes from './utils/fileSchemes'; +import * as vscode from 'vscode'; import { CachedNavTreeResponse } from './features/baseCodeLensProvider'; -import { memoize } from './utils/memoize'; +import { DiagnosticKind } from './features/diagnostics'; +import FileConfigurationManager from './features/fileConfigurationManager'; +import TypeScriptServiceClient from './typescriptServiceClient'; +import { CommandManager } from './utils/commandManager'; import { disposeAll } from './utils/dispose'; +import * as fileSchemes from './utils/fileSchemes'; +import { LanguageDescription } from './utils/languageDescription'; +import { memoize } from './utils/memoize'; import TelemetryReporter from './utils/telemetry'; +import TypingsStatus from './utils/typingsStatus'; + const validateSetting = 'validate.enable'; const suggestionSetting = 'suggestionActions.enabled'; export default class LanguageProvider { - private readonly diagnosticsManager: DiagnosticsManager; - - private _validate: boolean = true; - private _enableSuggestionDiagnostics: boolean = true; - private readonly disposables: vscode.Disposable[] = []; constructor( @@ -37,12 +32,6 @@ export default class LanguageProvider { private readonly typingsStatus: TypingsStatus, private readonly fileConfigurationManager: FileConfigurationManager ) { - this.client.bufferSyncSupport.onDelete(resource => { - this.diagnosticsManager.delete(resource); - }, null, this.disposables); - - this.diagnosticsManager = new DiagnosticsManager(description.diagnosticOwner); - vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables); this.configurationChanged(); @@ -53,8 +42,6 @@ export default class LanguageProvider { public dispose(): void { disposeAll(this.disposables); - - this.diagnosticsManager.dispose(); } @memoize @@ -84,14 +71,14 @@ export default class LanguageProvider { this.disposables.push((await import('./features/implementations')).register(selector, this.client)); this.disposables.push((await import('./features/implementationsCodeLens')).register(selector, this.description.id, this.client, cachedResponse)); this.disposables.push((await import('./features/jsDocCompletions')).register(selector, this.client, this.commandManager)); - this.disposables.push((await import('./features/organizeImports')).register(selector, this.client, this.commandManager, this.fileConfigurationManager)); - this.disposables.push((await import('./features/quickFix')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.diagnosticsManager, this.telemetryReporter)); + this.disposables.push((await import('./features/organizeImports')).register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter)); + this.disposables.push((await import('./features/quickFix')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter)); this.disposables.push((await import('./features/refactor')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter)); this.disposables.push((await import('./features/references')).register(selector, this.client)); this.disposables.push((await import('./features/referencesCodeLens')).register(selector, this.description.id, this.client, cachedResponse)); this.disposables.push((await import('./features/rename')).register(selector, this.client)); this.disposables.push((await import('./features/signatureHelp')).register(selector, this.client)); - this.disposables.push((await import('./features/tagCompletion')).register(selector, this.client)); + this.disposables.push((await import('./features/tagClosing')).register(selector, this.description.id, this.client)); this.disposables.push((await import('./features/typeDefinitions')).register(selector, this.client)); this.disposables.push((await import('./features/workspaceSymbols')).register(this.client, this.description.modeIds)); } @@ -120,30 +107,15 @@ export default class LanguageProvider { } private updateValidate(value: boolean) { - if (this._validate === value) { - return; - } - this._validate = value; - this.diagnosticsManager.validate = value; - if (value) { - this.triggerAllDiagnostics(); - } + this.client.diagnosticsManager.setValidate(this._diagnosticLanguage, value); } private updateSuggestionDiagnostics(value: boolean) { - if (this._enableSuggestionDiagnostics === value) { - return; - } - - this._enableSuggestionDiagnostics = value; - this.diagnosticsManager.enableSuggestions = value; - if (value) { - this.triggerAllDiagnostics(); - } + this.client.diagnosticsManager.setEnableSuggestions(this._diagnosticLanguage, value); } public reInitialize(): void { - this.diagnosticsManager.reInitialize(); + this.client.diagnosticsManager.reInitialize(); } public triggerAllDiagnostics(): void { @@ -153,7 +125,7 @@ export default class LanguageProvider { public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any })[]): void { const config = vscode.workspace.getConfiguration(this.id, file); const reportUnnecessary = config.get('showUnused', true); - this.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file, diagnostics.filter(diag => { + this.client.diagnosticsManager.updateDiagnostics(file, this._diagnosticLanguage, diagnosticsKind, diagnostics.filter(diag => { if (!reportUnnecessary) { diag.tags = undefined; if (diag.reportUnnecessary && diag.severity === vscode.DiagnosticSeverity.Hint) { @@ -165,6 +137,10 @@ export default class LanguageProvider { } public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void { - this.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics); + this.client.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics); + } + + private get _diagnosticLanguage() { + return this.description.diagnosticLanguage; } } \ No newline at end of file diff --git a/extensions/typescript-language-features/src/protocol.const.ts b/extensions/typescript-language-features/src/protocol.const.ts index 735e2aac259..c03c1ca9076 100644 --- a/extensions/typescript-language-features/src/protocol.const.ts +++ b/extensions/typescript-language-features/src/protocol.const.ts @@ -13,7 +13,6 @@ export class Kind { public static readonly directory = 'directory'; public static readonly enum = 'enum'; public static readonly externalModuleName = 'external module name'; - public static readonly file = 'file'; public static readonly function = 'function'; public static readonly indexSignature = 'index'; public static readonly interface = 'interface'; 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/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 1ce28c148e5..f675bd4f572 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -11,6 +11,7 @@ import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Disposable, Memento, Range, Uri, workspace } from 'vscode'; import { DiagnosticKind } from './features/diagnostics'; import FileConfigurationManager from './features/fileConfigurationManager'; +import { UpdateImportsOnFileRenameHandler } from './features/updatePathsOnRename'; import LanguageProvider from './languageProvider'; import * as Proto from './protocol'; import * as PConst from './protocol.const'; @@ -18,13 +19,12 @@ import TypeScriptServiceClient from './typescriptServiceClient'; import API from './utils/api'; import { CommandManager } from './utils/commandManager'; import { disposeAll } from './utils/dispose'; -import { LanguageDescription } from './utils/languageDescription'; +import { LanguageDescription, DiagnosticLanguage } from './utils/languageDescription'; import LogDirectoryProvider from './utils/logDirectoryProvider'; import { TypeScriptServerPlugin } from './utils/plugins'; import * as typeConverters from './utils/typeConverters'; import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; import VersionStatus from './utils/versionStatus'; -import { UpdateImportsOnFileRenameHandler } from './features/updatePathsOnRename'; // Style check diagnostics that can be reported as warnings const styleCheckDiagnostics = [ @@ -119,7 +119,8 @@ export default class TypeScriptServiceClientHost { const description: LanguageDescription = { id: 'typescript-plugins', modeIds: Array.from(languages.values()), - diagnosticSource: 'ts-plugins', + diagnosticSource: 'ts-plugin', + diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticOwner: 'typescript', isExternal: true }; @@ -286,8 +287,7 @@ export default class TypeScriptServiceClientHost { if (diagnostic.code) { converted.code = diagnostic.code; } - // TODO: requires TS 3.0 - const relatedInformation = (diagnostic as any).relatedInformation; + const relatedInformation = diagnostic.relatedInformation; if (relatedInformation) { converted.relatedInformation = relatedInformation.map((info: any) => { let span = info.span; diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 67be9e314c0..110f41d2658 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -3,18 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Uri, Event } from 'vscode'; +import { CancellationToken, Event, Uri } from 'vscode'; +import BufferSyncSupport from './features/bufferSyncSupport'; import * as Proto from './protocol'; import API from './utils/api'; -import { TypeScriptServerPlugin } from './utils/plugins'; import { TypeScriptServiceConfiguration } from './utils/configuration'; import Logger from './utils/logger'; -import BufferSyncSupport from './features/bufferSyncSupport'; - -declare module './protocol' { - export type JsxClosingTagRequestArgs = any; - export type JsxClosingTagResponse = any; -} +import { TypeScriptServerPlugin } from './utils/plugins'; export interface ITypeScriptServiceClient { /** @@ -56,6 +51,7 @@ export interface ITypeScriptServiceClient { execute(command: 'change', args: Proto.ChangeRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise; execute(command: 'quickinfo', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'completions', args: Proto.CompletionsRequestArgs, token?: CancellationToken): Promise; + execute(command: 'completionInfo', args: Proto.CompletionsRequestArgs, token?: CancellationToken): Promise; execute(command: 'completionEntryDetails', args: Proto.CompletionDetailsRequestArgs, token?: CancellationToken): Promise; execute(command: 'signatureHelp', args: Proto.SignatureHelpRequestArgs, token?: CancellationToken): Promise; execute(command: 'definition', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; @@ -64,7 +60,6 @@ export interface ITypeScriptServiceClient { execute(command: 'typeDefinition', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'references', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'navto', args: Proto.NavtoRequestArgs, token?: CancellationToken): Promise; - execute(command: 'navbar', args: Proto.FileRequestArgs, token?: CancellationToken): Promise; execute(command: 'format', args: Proto.FormatRequestArgs, token?: CancellationToken): Promise; execute(command: 'formatonkey', args: Proto.FormatOnKeyRequestArgs, token?: CancellationToken): Promise; execute(command: 'rename', args: Proto.RenameRequestArgs, token?: CancellationToken): Promise; diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index e152f0f6ca9..88ad494c3f6 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -4,34 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; -import * as path from 'path'; import * as fs from 'fs'; - -import * as electron from './utils/electron'; -import { Reader, ICallback } from './utils/wireProtocol'; - -import { workspace, window, Uri, CancellationToken, Disposable, Memento, MessageItem, EventEmitter, commands, env } from 'vscode'; +import * as path from 'path'; +import { CancellationToken, commands, Disposable, env, EventEmitter, Memento, MessageItem, Uri, window, workspace } from 'vscode'; +import * as nls from 'vscode-nls'; +import BufferSyncSupport from './features/bufferSyncSupport'; +import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'; import * as Proto from './protocol'; import { ITypeScriptServiceClient } from './typescriptService'; -import { TypeScriptServerPlugin } from './utils/plugins'; -import Logger from './utils/logger'; - +import API from './utils/api'; +import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; +import { disposeAll } from './utils/dispose'; +import * as electron from './utils/electron'; +import * as fileSchemes from './utils/fileSchemes'; import * as is from './utils/is'; +import LogDirectoryProvider from './utils/logDirectoryProvider'; +import Logger from './utils/logger'; +import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider'; +import { TypeScriptServerPlugin } from './utils/plugins'; import TelemetryReporter from './utils/telemetry'; import Tracer from './utils/tracer'; -import API from './utils/api'; - -import * as nls from 'vscode-nls'; -import { TypeScriptServiceConfiguration, TsServerLogLevel } from './utils/configuration'; -import { TypeScriptVersionProvider, TypeScriptVersion } from './utils/versionProvider'; -import { TypeScriptVersionPicker } from './utils/versionPicker'; -import * as fileSchemes from './utils/fileSchemes'; import { inferredProjectConfig } from './utils/tsconfig'; -import LogDirectoryProvider from './utils/logDirectoryProvider'; -import { disposeAll } from './utils/dispose'; -import { DiagnosticKind } from './features/diagnostics'; -import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider'; -import BufferSyncSupport from './features/bufferSyncSupport'; +import { TypeScriptVersionPicker } from './utils/versionPicker'; +import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'; +import { ICallback, Reader } from './utils/wireProtocol'; + + + + const localize = nls.loadMessageBundle(); @@ -197,6 +197,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private readonly disposables: Disposable[] = []; public readonly bufferSyncSupport: BufferSyncSupport; + public readonly diagnosticsManager: DiagnosticsManager; constructor( private readonly workspaceState: Memento, @@ -231,6 +232,11 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds); this.onReady(() => { this.bufferSyncSupport.listen(); }); + this.diagnosticsManager = new DiagnosticsManager('typescript'); + this.bufferSyncSupport.onDelete(resource => { + this.diagnosticsManager.delete(resource); + }, null, this.disposables); + workspace.onDidChangeConfiguration(() => { const oldConfiguration = this._configuration; this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); @@ -974,7 +980,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } if (this.apiVersion.gte(API.v222)) { - this.cancellationPipeName = electron.getTempSock('tscancellation'); + this.cancellationPipeName = electron.getTempFile('tscancellation'); args.push('--cancellationPipeName', this.cancellationPipeName + '*'); } diff --git a/extensions/typescript-language-features/src/utils/arrays.ts b/extensions/typescript-language-features/src/utils/arrays.ts index 57dbd54a29c..3a15e38981a 100644 --- a/extensions/typescript-language-features/src/utils/arrays.ts +++ b/extensions/typescript-language-features/src/utils/arrays.ts @@ -14,4 +14,8 @@ export function equals(one: T[], other: T[], itemEquals: (a: T, b: T) => bool } return true; +} + +export function flatten(arr: T[][]): T[] { + return [].concat.apply([], arr); } \ No newline at end of file diff --git a/extensions/typescript-language-features/src/utils/codeAction.ts b/extensions/typescript-language-features/src/utils/codeAction.ts index d31a40d602d..f9dec3a7d4d 100644 --- a/extensions/typescript-language-features/src/utils/codeAction.ts +++ b/extensions/typescript-language-features/src/utils/codeAction.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WorkspaceEdit, workspace } from 'vscode'; +import { workspace, WorkspaceEdit } from 'vscode'; import * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import * as typeConverters from './typeConverters'; diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index 9359a2bbed0..d5c0d00bdf3 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WorkspaceConfiguration, workspace } from 'vscode'; +import { workspace, WorkspaceConfiguration } from 'vscode'; import * as arrays from './arrays'; export enum TsServerLogLevel { diff --git a/extensions/typescript-language-features/src/utils/dependentRegistration.ts b/extensions/typescript-language-features/src/utils/dependentRegistration.ts index 9013dd28bc9..232d6b8e2cf 100644 --- a/extensions/typescript-language-features/src/utils/dependentRegistration.ts +++ b/extensions/typescript-language-features/src/utils/dependentRegistration.ts @@ -8,7 +8,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import API from './api'; import { disposeAll } from './dispose'; -class ConditionalRegistration { +export class ConditionalRegistration { private registration: vscode.Disposable | undefined = undefined; public constructor( @@ -75,12 +75,8 @@ export class ConfigurationDependentRegistration { register: () => vscode.Disposable, ) { this._registration = new ConditionalRegistration(register); - this.update(); - - vscode.workspace.onDidChangeConfiguration(() => { - this.update(); - }, null, this._disposables); + vscode.workspace.onDidChangeConfiguration(this.update, this, this._disposables); } public dispose() { diff --git a/extensions/typescript-language-features/src/utils/electron.ts b/extensions/typescript-language-features/src/utils/electron.ts index 34a6534f8ec..46129f60f0e 100644 --- a/extensions/typescript-language-features/src/utils/electron.ts +++ b/extensions/typescript-language-features/src/utils/electron.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import Logger from './logger'; -import { getTempFile, makeRandomHexString } from './temp'; +import * as temp from './temp'; import path = require('path'); -import os = require('os'); +import fs = require('fs'); import net = require('net'); import cp = require('child_process'); @@ -15,13 +15,25 @@ export interface IForkOptions { execArgv?: string[]; } -export function getTempSock(prefix: string): string { - const fullName = `vscode-${prefix}-${makeRandomHexString(20)}`; - return getTempFile(fullName + '.sock'); +const getRootTempDir = (() => { + let dir: string | undefined; + return () => { + if (!dir) { + dir = temp.getTempFile(`vscode-typescript`); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + } + return dir; + }; +})(); + +export function getTempFile(prefix: string): string { + return path.join(getRootTempDir(), `${prefix}-${temp.makeRandomHexString(20)}.tmp`); } function generatePipeName(): string { - return getPipeName(makeRandomHexString(40)); + return getPipeName(temp.makeRandomHexString(40)); } function getPipeName(name: string): string { @@ -31,7 +43,7 @@ function getPipeName(name: string): string { } // Mac/Unix: use socket file - return path.join(os.tmpdir(), fullName + '.sock'); + return path.join(getRootTempDir(), fullName + '.sock'); } function generatePatchedEnv( diff --git a/extensions/typescript-language-features/src/utils/languageDescription.ts b/extensions/typescript-language-features/src/utils/languageDescription.ts index cd84e9b5f47..f6f4806ae03 100644 --- a/extensions/typescript-language-features/src/utils/languageDescription.ts +++ b/extensions/typescript-language-features/src/utils/languageDescription.ts @@ -4,26 +4,36 @@ *--------------------------------------------------------------------------------------------*/ import * as languageModeIds from './languageModeIds'; +export enum DiagnosticLanguage { + JavaScript, + TypeScript +} + +export const allDiagnosticLangauges = [DiagnosticLanguage.JavaScript, DiagnosticLanguage.TypeScript]; + export interface LanguageDescription { readonly id: string; + readonly diagnosticOwner: string; readonly diagnosticSource: string; + readonly diagnosticLanguage: DiagnosticLanguage; readonly modeIds: string[]; readonly configFile?: string; readonly isExternal?: boolean; - readonly diagnosticOwner: string; } export const standardLanguageDescriptions: LanguageDescription[] = [ { id: 'typescript', - diagnosticSource: 'ts', diagnosticOwner: 'typescript', + diagnosticSource: 'ts', + diagnosticLanguage: DiagnosticLanguage.TypeScript, modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact], configFile: 'tsconfig.json' }, { id: 'javascript', - diagnosticSource: 'ts', diagnosticOwner: 'typescript', + diagnosticSource: 'ts', + diagnosticLanguage: DiagnosticLanguage.JavaScript, modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact], configFile: 'jsconfig.json' } diff --git a/extensions/typescript-language-features/src/utils/languageModeIds.ts b/extensions/typescript-language-features/src/utils/languageModeIds.ts index 5fe187e13db..9be48067eda 100644 --- a/extensions/typescript-language-features/src/utils/languageModeIds.ts +++ b/extensions/typescript-language-features/src/utils/languageModeIds.ts @@ -14,4 +14,8 @@ export const jsxTags = 'jsx-tags'; export function isSupportedLanguageMode(doc: vscode.TextDocument) { return vscode.languages.match([typescript, typescriptreact, javascript, javascriptreact], doc) > 0; +} + +export function isTypeScriptDocument(doc: vscode.TextDocument) { + return vscode.languages.match([typescript, typescriptreact], doc) > 0; } \ No newline at end of file diff --git a/extensions/typescript-language-features/src/utils/logger.ts b/extensions/typescript-language-features/src/utils/logger.ts index cc57346efbf..782fe871fe3 100644 --- a/extensions/typescript-language-features/src/utils/logger.ts +++ b/extensions/typescript-language-features/src/utils/logger.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { OutputChannel, window } from 'vscode'; +import * as nls from 'vscode-nls'; import * as is from './is'; import { memoize } from './memoize'; -import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); export default class Logger { diff --git a/extensions/typescript-language-features/src/utils/pluginPathsProvider.ts b/extensions/typescript-language-features/src/utils/pluginPathsProvider.ts index e2e39d68801..d001512234b 100644 --- a/extensions/typescript-language-features/src/utils/pluginPathsProvider.ts +++ b/extensions/typescript-language-features/src/utils/pluginPathsProvider.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; import { workspace } from 'vscode'; - import { TypeScriptServiceConfiguration } from './configuration'; import { RelativeWorkspacePathResolver } from './relativePathResolver'; + export class TypeScriptPluginPathsProvider { public readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver(); diff --git a/extensions/typescript-language-features/src/utils/previewer.ts b/extensions/typescript-language-features/src/utils/previewer.ts index 647ae3bf91b..df0105ac287 100644 --- a/extensions/typescript-language-features/src/utils/previewer.ts +++ b/extensions/typescript-language-features/src/utils/previewer.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as Proto from '../protocol'; import { MarkdownString } from 'vscode'; +import * as Proto from '../protocol'; function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined { if (!tag.text) { @@ -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]; @@ -72,13 +72,18 @@ export function markdownDocumentation( export function addMarkdownDocumentation( out: MarkdownString, - documentation: Proto.SymbolDisplayPart[], - tags: Proto.JSDocTagInfo[] + documentation: Proto.SymbolDisplayPart[] | undefined, + tags: Proto.JSDocTagInfo[] | undefined ): MarkdownString { - out.appendMarkdown(plain(documentation)); - const tagsPreview = tagsMarkdownPreview(tags); - if (tagsPreview) { - out.appendMarkdown('\n\n' + tagsPreview); + if (documentation) { + out.appendMarkdown(plain(documentation)); + } + + if (tags) { + const tagsPreview = tagsMarkdownPreview(tags); + if (tagsPreview) { + out.appendMarkdown('\n\n' + tagsPreview); + } } return out; -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/src/utils/projectStatus.ts b/extensions/typescript-language-features/src/utils/projectStatus.ts index 8fbc39d2ce6..4eb9b8c7686 100644 --- a/extensions/typescript-language-features/src/utils/projectStatus.ts +++ b/extensions/typescript-language-features/src/utils/projectStatus.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; import { loadMessageBundle } from 'vscode-nls'; -import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './tsconfig'; +import { ITypeScriptServiceClient } from '../typescriptService'; import TelemetryReporter from './telemetry'; +import { isImplicitProjectConfigFile, openOrCreateConfigFile } from './tsconfig'; const localize = loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/features/resourceMap.ts b/extensions/typescript-language-features/src/utils/resourceMap.ts similarity index 92% rename from extensions/typescript-language-features/src/features/resourceMap.ts rename to extensions/typescript-language-features/src/utils/resourceMap.ts index f2b9c6fd711..f59ab661f5a 100644 --- a/extensions/typescript-language-features/src/features/resourceMap.ts +++ b/extensions/typescript-language-features/src/utils/resourceMap.ts @@ -5,8 +5,8 @@ import * as fs from 'fs'; import { Uri } from 'vscode'; -import { memoize } from '../utils/memoize'; -import { getTempFile } from '../utils/temp'; +import { memoize } from './memoize'; +import { getTempFile } from './temp'; /** * Maps of file resources @@ -45,12 +45,16 @@ export class ResourceMap { } } + public clear() { + this._map.clear(); + } + public get values(): Iterable { return this._map.values(); } - public get keys(): Iterable { - return this._map.keys(); + public get entries() { + return this._map.entries(); } private toKey(resource: Uri): string | null { diff --git a/extensions/typescript-language-features/src/utils/tracer.ts b/extensions/typescript-language-features/src/utils/tracer.ts index 97ce34a2abd..2c12e2bf15a 100644 --- a/extensions/typescript-language-features/src/utils/tracer.ts +++ b/extensions/typescript-language-features/src/utils/tracer.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { workspace } from 'vscode'; - import * as Proto from '../protocol'; import Logger from './logger'; + enum Trace { Off, Messages, diff --git a/extensions/typescript-language-features/src/utils/tsconfig.ts b/extensions/typescript-language-features/src/utils/tsconfig.ts index 91293a8cc11..bc2f9c232a1 100644 --- a/extensions/typescript-language-features/src/utils/tsconfig.ts +++ b/extensions/typescript-language-features/src/utils/tsconfig.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import * as path from 'path'; +import * as vscode from 'vscode'; import * as Proto from '../protocol'; - import { TypeScriptServiceConfiguration } from './configuration'; + export function isImplicitProjectConfigFile(configFileName: string) { return configFileName.indexOf('/dev/null/') === 0; } diff --git a/extensions/typescript-language-features/src/utils/typeConverters.ts b/extensions/typescript-language-features/src/utils/typeConverters.ts index 8c87338893d..44ad7f4ae5e 100644 --- a/extensions/typescript-language-features/src/utils/typeConverters.ts +++ b/extensions/typescript-language-features/src/utils/typeConverters.ts @@ -24,6 +24,14 @@ export namespace Range { endLine: range.end.line + 1, endOffset: range.end.character + 1 }); + + export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({ + file, + line: range.start.line + 1, + offset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }); } export namespace Position { diff --git a/extensions/typescript-language-features/src/utils/typingsStatus.ts b/extensions/typescript-language-features/src/utils/typingsStatus.ts index cde711cd61b..0113435deda 100644 --- a/extensions/typescript-language-features/src/utils/typingsStatus.ts +++ b/extensions/typescript-language-features/src/utils/typingsStatus.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MessageItem, workspace, Disposable, ProgressLocation, window } from 'vscode'; -import { ITypeScriptServiceClient } from '../typescriptService'; +import { Disposable, MessageItem, ProgressLocation, window, workspace } from 'vscode'; import { loadMessageBundle } from 'vscode-nls'; +import { ITypeScriptServiceClient } from '../typescriptService'; const localize = loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/utils/versionPicker.ts b/extensions/typescript-language-features/src/utils/versionPicker.ts index f48aae90c27..0702a60e533 100644 --- a/extensions/typescript-language-features/src/utils/versionPicker.ts +++ b/extensions/typescript-language-features/src/utils/versionPicker.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { commands, Memento, QuickPickItem, Uri, window, workspace } from 'vscode'; import * as nls from 'vscode-nls'; -import { TypeScriptVersionProvider, TypeScriptVersion } from './versionProvider'; -import { Memento, commands, Uri, window, QuickPickItem, workspace } from 'vscode'; +import { TypeScriptVersion, TypeScriptVersionProvider } from './versionProvider'; const localize = nls.loadMessageBundle(); diff --git a/extensions/typescript-language-features/src/utils/versionProvider.ts b/extensions/typescript-language-features/src/utils/versionProvider.ts index 25ecb3355d1..27b2d9fdebe 100644 --- a/extensions/typescript-language-features/src/utils/versionProvider.ts +++ b/extensions/typescript-language-features/src/utils/versionProvider.ts @@ -2,17 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import * as path from 'path'; import * as fs from 'fs'; - -import { workspace, window } from 'vscode'; - +import * as path from 'path'; +import { window, workspace } from 'vscode'; +import * as nls from 'vscode-nls'; +import API from './api'; import { TypeScriptServiceConfiguration } from './configuration'; import { RelativeWorkspacePathResolver } from './relativePathResolver'; -import API from './api'; +const localize = nls.loadMessageBundle(); + + + export class TypeScriptVersion { diff --git a/extensions/typescript-language-features/src/utils/versionStatus.ts b/extensions/typescript-language-features/src/utils/versionStatus.ts index d852137cb65..450b50a2dc1 100644 --- a/extensions/typescript-language-features/src/utils/versionStatus.ts +++ b/extensions/typescript-language-features/src/utils/versionStatus.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { TypeScriptVersion } from './versionProvider'; import * as languageModeIds from './languageModeIds'; +import { TypeScriptVersion } from './versionProvider'; export default class VersionStatus { private readonly _onChangeEditorSub: vscode.Disposable; diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 39422f56063..3f1d2b4ed86 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -835,6 +835,10 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" +jsonc-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.1.tgz#9d23cd2709714fff508a1a6679d82135bee1ae60" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 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 2e9fe0d0bbf..a350f5334e2 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -499,17 +499,16 @@ suite('workspace-namespace', () => { }); }); - // TODO@Joh this test fails randomly - // test('findFiles, cancellation', () => { + test('findFiles, cancellation', () => { - // const source = new CancellationTokenSource(); - // const token = source.token; // just to get an instance first - // source.cancel(); + const source = new vscode.CancellationTokenSource(); + const token = source.token; // just to get an instance first + source.cancel(); - // return vscode.workspace.findFiles('*.js', null, 100, token).then((res) => { - // assert.equal(res, void 0); - // }); - // }); + return vscode.workspace.findFiles('*.js', null, 100, token).then((res) => { + assert.deepEqual(res, []); + }); + }); test('findTextInFiles', async () => { const results: vscode.TextSearchResult[] = []; @@ -694,9 +693,52 @@ suite('workspace-namespace', () => { test('WorkspaceEdit: create & ignoreIfExists', async function () { let docUri = await createRandomFile('before'); + let we = new vscode.WorkspaceEdit(); we.createFile(docUri, { ignoreIfExists: true }); assert.ok(await vscode.workspace.applyEdit(we)); assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), 'before'); + + we = new vscode.WorkspaceEdit(); + we.createFile(docUri, { overwrite: true, ignoreIfExists: true }); + assert.ok(await vscode.workspace.applyEdit(we)); + assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), ''); + }); + + test('WorkspaceEdit: rename & ignoreIfExists', async function () { + let aUri = await createRandomFile('aaa'); + let bUri = await createRandomFile('bbb'); + + let we = new vscode.WorkspaceEdit(); + we.renameFile(aUri, bUri); + assert.ok(!await vscode.workspace.applyEdit(we)); + + we = new vscode.WorkspaceEdit(); + we.renameFile(aUri, bUri, { ignoreIfExists: true }); + assert.ok(await vscode.workspace.applyEdit(we)); + + we = new vscode.WorkspaceEdit(); + we.renameFile(aUri, bUri, { overwrite: false, ignoreIfExists: true }); + assert.ok(!await vscode.workspace.applyEdit(we)); + + we = new vscode.WorkspaceEdit(); + we.renameFile(aUri, bUri, { overwrite: true, ignoreIfExists: true }); + assert.ok(await vscode.workspace.applyEdit(we)); + }); + + test('WorkspaceEdit: delete & ignoreIfNotExists', async function () { + + let docUri = await createRandomFile(); + let we = new vscode.WorkspaceEdit(); + we.deleteFile(docUri, { ignoreIfNotExists: false }); + assert.ok(await vscode.workspace.applyEdit(we)); + + we = new vscode.WorkspaceEdit(); + we.deleteFile(docUri, { ignoreIfNotExists: false }); + assert.ok(!await vscode.workspace.applyEdit(we)); + + we = new vscode.WorkspaceEdit(); + we.deleteFile(docUri, { ignoreIfNotExists: true }); + assert.ok(await vscode.workspace.applyEdit(we)); }); }); diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 7ec0734b094..6ed3a9d31c4 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,6 +2,6 @@ # yarn lockfile v1 -typescript@2.9.2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" +typescript@3.0.1-insiders.20180713: + version "3.0.1-insiders.20180713" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1-insiders.20180713.tgz#02775b5197ab02b79ed5b84e7483c18ed164e55e" diff --git a/gulpfile.js b/gulpfile.js index db6d924ae73..ebdca25bcb4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,12 +15,12 @@ const compilation = require('./build/lib/compilation'); // Fast compile for development time gulp.task('clean-client', util.rimraf('out')); -gulp.task('compile-client', ['clean-client'], compilation.compileTask('out', false)); +gulp.task('compile-client', ['clean-client'], compilation.compileTask('src', 'out', false)); gulp.task('watch-client', ['clean-client'], compilation.watchTask('out', false)); // Full compile, including nls and inline sources in sourcemaps, for build gulp.task('clean-client-build', util.rimraf('out-build')); -gulp.task('compile-client-build', ['clean-client-build'], compilation.compileTask('out-build', true)); +gulp.task('compile-client-build', ['clean-client-build'], compilation.compileTask('src', 'out-build', true)); gulp.task('watch-client-build', ['clean-client-build'], compilation.watchTask('out-build', true)); // Default diff --git a/package.json b/package.json index 54df1ab121c..a0af247bb1f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.26.0", - "distro": "7ee5cda9f4f44b2ba1f1ab290e48f86416914794", + "distro": "ba277984505f7010dbff765d57412a25891b4dea", "author": { "name": "Microsoft Corporation" }, @@ -13,6 +13,7 @@ "postinstall": "node build/npm/postinstall.js", "compile": "gulp compile --max_old_space_size=4095", "watch": "gulp watch --max_old_space_size=4095", + "watch-client": "gulp watch-client --max_old_space_size=4095", "monaco-editor-test": "mocha --only-monaco-editor", "precommit": "node build/gulpfile.hygiene.js", "gulp": "gulp --max_old_space_size=4095", @@ -41,7 +42,7 @@ "native-watchdog": "0.3.0", "node-pty": "0.7.6", "semver": "^5.5.0", - "spdlog": "0.6.0", + "spdlog": "0.7.1", "sudo-prompt": "8.2.0", "v8-inspect-profiler": "^0.0.8", "vscode-chokidar": "1.6.2", @@ -49,7 +50,7 @@ "vscode-nsfw": "1.0.17", "vscode-ripgrep": "^1.0.1", "vscode-textmate": "^4.0.1", - "vscode-xterm": "3.6.0-beta1", + "vscode-xterm": "3.6.0-beta3", "yauzl": "^2.9.1" }, "devDependencies": { @@ -137,4 +138,4 @@ "windows-mutex": "^0.2.0", "windows-process-tree": "0.2.2" } -} +} \ No newline at end of file diff --git a/product.json b/product.json index d25b0eca0f2..d16dca66ecd 100644 --- a/product.json +++ b/product.json @@ -4,6 +4,7 @@ "applicationName": "code-oss", "dataFolderName": ".vscode-oss", "win32MutexName": "vscodeoss", + "enableTelemetry": true, "licenseName": "MIT", "licenseUrl": "https://github.com/Microsoft/vscode/blob/master/LICENSE.txt", "win32DirName": "Microsoft Code OSS", @@ -22,4 +23,4 @@ "ms-vscode.node-debug", "ms-vscode.node-debug2" ] -} \ No newline at end of file +} diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index 98bc02c22cd..752d996f6b9 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -69,6 +69,11 @@ loader.config({ nodeCachedDataDir: process.env['VSCODE_NODE_CACHED_DATA_DIR_' + process.pid] }); +if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions.electron) { + // running in Electron + loader.define('fs', ['original-fs'], function (originalFS) { return originalFS; }); // replace the patched electron fs with the original node fs for all AMD code +} + if (nlsConfig.pseudo) { loader(['vs/nls'], function (nlsPlugin) { nlsPlugin.setPseudoTranslation(nlsConfig.pseudo); diff --git a/src/main.js b/src/main.js index 5d30c39f692..850cda67f2d 100644 --- a/src/main.js +++ b/src/main.js @@ -93,6 +93,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; @@ -183,8 +196,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) => { @@ -203,8 +215,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) { @@ -243,8 +254,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. @@ -285,7 +294,7 @@ 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'); @@ -395,23 +404,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/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 02b3d4115ea..e8ed9bec491 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -840,6 +840,8 @@ export const EventType = { SUBMIT: 'submit', RESET: 'reset', FOCUS: 'focus', + FOCUS_IN: 'focusin', + FOCUS_OUT: 'focusout', BLUR: 'blur', INPUT: 'input', // Local Storage diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index b5266e63759..72742b71751 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -11,7 +11,7 @@ import * as nls from 'vs/nls'; import * as lifecycle from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { Builder, $ } from 'vs/base/browser/builder'; -import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; +import { SelectBox, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox'; import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, IRunEvent } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import * as types from 'vs/base/common/types'; @@ -496,6 +496,28 @@ export class ActionBar implements IActionRunner { this.actionsList.setAttribute('aria-label', this.options.ariaLabel); } + if (this.options.isMenu) { + $(this.actionsList).on(DOM.EventType.MOUSE_OVER, (e) => { + let target = e.target as HTMLElement; + if (!target || !DOM.isAncestor(target, this.actionsList) || target === this.actionsList) { + return; + } + + while (target.parentElement !== this.actionsList) { + target = target.parentElement; + } + + if (DOM.hasClass(target, 'action-item') && !DOM.hasClass(target, 'disabled')) { + const lastFocusedItem = this.focusedItem; + this.setFocusedItem(target); + + if (lastFocusedItem !== this.focusedItem) { + this.updateFocus(); + } + } + }); + } + this.domNode.appendChild(this.actionsList); container.appendChild(this.domNode); @@ -525,6 +547,16 @@ export class ActionBar implements IActionRunner { } } + private setFocusedItem(element: HTMLElement): void { + for (let i = 0; i < this.actionsList.children.length; i++) { + let elem = this.actionsList.children[i]; + if (element === elem) { + this.focusedItem = i; + break; + } + } + } + private updateFocusedItem(): void { for (let i = 0; i < this.actionsList.children.length; i++) { let elem = this.actionsList.children[i]; @@ -762,10 +794,10 @@ export class SelectActionItem extends BaseActionItem { protected selectBox: SelectBox; protected toDispose: lifecycle.IDisposable[]; - constructor(ctx: any, action: IAction, options: string[], selected: number, contextViewProvider: IContextViewProvider + constructor(ctx: any, action: IAction, options: string[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions ) { super(ctx, action); - this.selectBox = new SelectBox(options, selected, contextViewProvider); + this.selectBox = new SelectBox(options, selected, contextViewProvider, null, selectBoxOptions); this.toDispose = []; this.toDispose.push(this.selectBox); 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..d268c209a15 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.css @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + outline-style: none; +} + +.monaco-breadcrumbs .monaco-breadcrumb-item { + display: flex; + align-items: center; + flex: 0 1 auto; + white-space: nowrap; + cursor: pointer; + align-self: center; + height: 100%; +} + +.monaco-breadcrumbs .monaco-breadcrumb-item:not(:first-child)::before { + background-image: url(./collapsed.svg); + opacity: .7; + width: 16px; + height: 16px; + display: inline-block; + background-size: 16px; + background-position: 50% 50%; + content: ' '; +} + +.vs-dark .monaco-breadcrumbs .monaco-breadcrumb-item:not(:first-child)::before { + background-image: url(./collpased-dark.svg); +} + 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..0966ceca010 --- /dev/null +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -0,0 +1,291 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + breadcrumbsForeground?: Color; + breadcrumbsHoverBackground?: Color; + breadcrumbsHoverForeground?: Color; + breadcrumbsFocusForeground?: Color; + breadcrumbsFocusAndSelectionBackground?: Color; + breadcrumbsFocusAndSelectionForeground?: Color; +} + +export interface IBreadcrumbsItemEvent { + type: 'select' | 'focus'; + item: BreadcrumbsItem; + node: HTMLElement; + payload: any; +} + +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._domNode.style.width = `${dim.width}px`; + this._domNode.style.height = `${dim.height}px`; + } + this._scrollable.setRevealOnScroll(false); + this._scrollable.scanDomNode(); + this._scrollable.setRevealOnScroll(true); + } + + style(style: IBreadcrumbsWidgetStyles): void { + let content = ''; + if (style.breadcrumbsBackground) { + content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`; + } + if (style.breadcrumbsForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsForeground}}\n`; + } + if (style.breadcrumbsFocusForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusForeground}}\n`; + } + if (style.breadcrumbsFocusAndSelectionBackground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { background-color: ${style.breadcrumbsFocusAndSelectionBackground}}\n`; + } + if (style.breadcrumbsFocusAndSelectionForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { color: ${style.breadcrumbsFocusAndSelectionForeground}}\n`; + } + if (style.breadcrumbsHoverBackground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { background-color: ${style.breadcrumbsHoverBackground}}\n`; + } + if (style.breadcrumbsHoverForeground) { + content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\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(); + } + + isDOMFocused(): boolean { + return this._domNode === document.activeElement; + } + + getFocused(): BreadcrumbsItem { + return this._items[this._focusedItemIdx]; + } + + setFocused(item: BreadcrumbsItem, payload?: any): void { + this._focus(this._items.indexOf(item), payload); + } + + focusPrev(payload?: any): any { + if (this._focusedItemIdx > 0) { + this._focus(this._focusedItemIdx - 1, payload); + } + } + + focusNext(payload?: any): any { + if (this._focusedItemIdx + 1 < this._nodes.length) { + this._focus(this._focusedItemIdx + 1, payload); + } + } + + private _focus(nth: number, payload: any): 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._reveal(this._focusedItemIdx); + this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx], payload }); + } + + reveal(item: BreadcrumbsItem): void { + let idx = this._items.indexOf(item); + if (idx >= 0) { + this._reveal(idx); + } + } + + private _reveal(nth: number): void { + const node = this._nodes[nth]; + if (node) { + this._scrollable.setRevealOnScroll(false); + this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft }); + this._scrollable.setRevealOnScroll(true); + } + } + + getSelection(): BreadcrumbsItem { + return this._items[this._selectedItemIdx]; + } + + setSelection(item: BreadcrumbsItem, payload?: any): void { + this._select(this._items.indexOf(item), payload); + } + + private _select(nth: number, payload: any): 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({ type: 'select', item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx], payload }); + } + + getItems(): ReadonlyArray { + return this._items; + } + + 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); + this._focus(-1, undefined); + } + + 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, event); + this._select(idx, event); + 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/contextview/contextview.css b/src/vs/base/browser/ui/contextview/contextview.css index 84942adb3e6..af3ead17a6b 100644 --- a/src/vs/base/browser/ui/contextview/contextview.css +++ b/src/vs/base/browser/ui/contextview/contextview.css @@ -5,5 +5,5 @@ .context-view { position: absolute; - z-index: 1000; + z-index: 2000; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index b5b1c296425..8a14b87c36c 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -8,7 +8,7 @@ import 'vs/css!./contextview'; import { Builder, $ } from 'vs/base/browser/builder'; import * as DOM from 'vs/base/browser/dom'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; export interface IAnchor { x: number; @@ -116,11 +116,9 @@ export class ContextView { this.$view = $('.context-view').hide(); this.setContainer(container); - this.toDispose = [{ - dispose: () => { - this.setContainer(null); - } - }]; + this.toDispose = [toDisposable(() => { + this.setContainer(null); + })]; this.toDisposeOnClean = null; } 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/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 8d62877e57d..cb972251f77 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -22,6 +22,7 @@ export interface IIconLabelCreationOptions { export interface IIconLabelValueOptions { title?: string; descriptionTitle?: string; + hideIcon?: boolean; extraClasses?: string[]; italic?: boolean; matches?: IMatch[]; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index de20e20ffc6..17b36ec3dea 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -524,6 +524,10 @@ export class HistoryInputBox extends InputBox implements IHistoryNavigationWidge } public showNextValue(): void { + if (!this.history.has(this.value)) { + this.addToHistory(); + } + let next = this.getNextValue(); if (next) { next = next === this.value ? this.getNextValue() : next; diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index df2fbfde7d3..37ff5439232 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -5,7 +5,7 @@ import { GestureEvent } from 'vs/base/browser/touch'; -export interface IDelegate { +export interface IVirtualDelegate { getHeight(element: T): number; getTemplateId(element: T): string; } @@ -14,6 +14,7 @@ export interface IRenderer { templateId: string; renderTemplate(container: HTMLElement): TTemplateData; renderElement(element: TElement, index: number, templateData: TTemplateData): void; + disposeElement(element: TElement, index: number, templateData: TTemplateData): void; disposeTemplate(templateData: TTemplateData): void; } diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 53ea8be6a37..10094f4e5a6 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -6,7 +6,7 @@ import 'vs/css!./list'; import { IDisposable } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; -import { IDelegate, IRenderer, IListEvent, IListOpenEvent } from './list'; +import { IVirtualDelegate, IRenderer, IListEvent, IListOpenEvent } from './list'; import { List, IListStyles, IListOptions } from './listWidget'; import { IPagedModel } from 'vs/base/common/paging'; import { Event, mapEvent } from 'vs/base/common/event'; @@ -50,6 +50,10 @@ class PagedRenderer implements IRenderer this.renderer.renderElement(entry, index, data.data)); } + disposeElement(): void { + // noop + } + disposeTemplate(data: ITemplateData): void { data.disposable.dispose(); data.disposable = null; @@ -65,12 +69,12 @@ export class PagedList implements IDisposable { constructor( container: HTMLElement, - delegate: IDelegate, + virtualDelegate: IVirtualDelegate, renderers: IPagedRenderer[], options: IListOptions = {} ) { const pagedRenderers = renderers.map(r => new PagedRenderer>(r, () => this.model)); - this.list = new List(container, delegate, pagedRenderers, options); + this.list = new List(container, virtualDelegate, pagedRenderers, options); } getHTMLElement(): HTMLElement { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 1d560a8afec..92d8d99d64b 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -12,7 +12,7 @@ import { domEvent } from 'vs/base/browser/event'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable'; import { RangeMap, IRange, relativeComplement, intersect, shift } from './rangeMap'; -import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list'; +import { IVirtualDelegate, IRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list'; import { RowCache, IRow } from './rowCache'; import { isWindows } from 'vs/base/common/platform'; import * as browser from 'vs/base/browser/browser'; @@ -72,7 +72,7 @@ export class ListView implements ISpliceable, IDisposable { constructor( container: HTMLElement, - private delegate: IDelegate, + private virtualDelegate: IVirtualDelegate, renderers: IRenderer[], options: IListViewOptions = DefaultOptions ) { @@ -156,8 +156,8 @@ export class ListView implements ISpliceable, IDisposable { const inserted = elements.map>(element => ({ id: String(this.itemId++), element, - size: this.delegate.getHeight(element), - templateId: this.delegate.getTemplateId(element), + size: this.virtualDelegate.getHeight(element), + templateId: this.virtualDelegate.getTemplateId(element), row: null })); @@ -311,6 +311,12 @@ export class ListView implements ISpliceable, IDisposable { private removeItemFromDOM(index: number): void { const item = this.items[index]; + const renderer = this.renderers.get(item.templateId); + + if (renderer.disposeElement) { + renderer.disposeElement(item.element, index, item.row.templateData); + } + this.cache.release(item.row); item.row = null; } @@ -371,7 +377,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/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index d297818a787..efaf1d52986 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./list'; +import { localize } from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isNumber } from 'vs/base/common/types'; import { range, firstIndex } from 'vs/base/common/arrays'; @@ -15,7 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Event, Emitter, EventBufferer, chain, mapEvent, anyEvent } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { IDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListOpenEvent } from './list'; +import { IVirtualDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListOpenEvent } from './list'; import { ListView, IListViewOptions } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -68,6 +69,10 @@ class TraitRenderer implements IRenderer this.trait.renderIndex(index, templateData); } + disposeElement(): void { + // noop + } + splice(start: number, deleteCount: number, insertCount: number): void { const rendered: IRenderedContainer[] = []; @@ -351,6 +356,11 @@ class DOMFocusController implements IDisposable { return; } + const style = window.getComputedStyle(tabIndexElement); + if (style.visibility === 'hidden' || style.display === 'none') { + return; + } + e.preventDefault(); e.stopPropagation(); tabIndexElement.focus(); @@ -807,6 +817,14 @@ class PipelineRenderer implements IRenderer { } } + disposeElement(element: T, index: number, templateData: any[]): void { + let i = 0; + + for (const renderer of this.renderers) { + renderer.disposeElement(element, index, templateData[i++]); + } + } + disposeTemplate(templateData: any[]): void { let i = 0; @@ -871,7 +889,7 @@ export class List implements ISpliceable, IDisposable { constructor( container: HTMLElement, - delegate: IDelegate, + virtualDelegate: IVirtualDelegate, renderers: IRenderer[], options: IListOptions = DefaultOptions ) { @@ -882,7 +900,7 @@ export class List implements ISpliceable, IDisposable { renderers = renderers.map(r => new PipelineRenderer(r.templateId, [this.focus.renderer, this.selection.renderer, r])); - this.view = new ListView(container, delegate, renderers, options); + this.view = new ListView(container, virtualDelegate, renderers, options); this.view.domNode.setAttribute('role', 'tree'); DOM.addClass(this.view.domNode, this.idPrefix); this.view.domNode.tabIndex = 0; @@ -922,13 +940,21 @@ export class List implements ISpliceable, IDisposable { this.onSelectionChange(this._onSelectionChange, this, this.disposables); if (options.ariaLabel) { - this.view.domNode.setAttribute('aria-label', options.ariaLabel); + this.view.domNode.setAttribute('aria-label', localize('aria list', "{0}. Use the navigation keys to navigate.", options.ariaLabel)); } this.style(options); } splice(start: number, deleteCount: number, elements: T[] = []): void { + if (start < 0 || start > this.view.length) { + throw new Error(`Invalid start index: ${start}`); + } + + if (deleteCount < 0) { + throw new Error(`Invalid delete count: ${deleteCount}`); + } + if (deleteCount === 0 && elements.length === 0) { return; } diff --git a/src/vs/base/browser/ui/list/splice.ts b/src/vs/base/browser/ui/list/splice.ts index e65be2e8432..254e25d6a7b 100644 --- a/src/vs/base/browser/ui/list/splice.ts +++ b/src/vs/base/browser/ui/list/splice.ts @@ -5,9 +5,7 @@ 'use strict'; -export interface ISpliceable { - splice(start: number, deleteCount: number, elements: T[]): void; -} +import { ISpliceable } from 'vs/base/common/sequence'; export interface ISpreadSpliceable { splice(start: number, deleteCount: number, ...elements: T[]): void; diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css index fff01a4a278..76f2599c526 100644 --- a/src/vs/base/browser/ui/menu/menu.css +++ b/src/vs/base/browser/ui/menu/menu.css @@ -35,17 +35,23 @@ background-color: #E4E4E4; } -.monaco-menu .monaco-action-bar.vertical .action-item:hover:not(.disabled) { - background-color: #EEE; +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -ms-flexbox; + display: flex; + height: 2.6em; + align-items: center; } .monaco-menu .monaco-action-bar.vertical .action-label { -ms-flex: 1 1 auto; flex: 1 1 auto; text-decoration: none; - padding: 0.8em 1em; - line-height: 1.1em; + padding: 0 1em; background: none; + font-size: inherit; + line-height: 1; } .monaco-menu .monaco-action-bar.vertical .keybinding, @@ -53,15 +59,12 @@ display: inline-block; -ms-flex: 2 1 auto; flex: 2 1 auto; - padding: 0.8em 1em; - line-height: 1.1em; - font-size: 12px; + padding: 0 1em; text-align: right; + font-size: inherit; + line-height: 1; } -.monaco-menu .monaco-action-bar.vertical .submenu-indicator { - padding: 0.8em .5em; -} .monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding, .monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator { @@ -118,15 +121,15 @@ outline: 0; } +.monaco-menu .monaco-action-bar.vertical .action-item { + border: 1px solid transparent; /* prevents jumping behaviour on hover or focus */ +} + /* Dark theme */ .vs-dark .monaco-menu .monaco-action-bar.vertical .action-item.focused { background-color: #4B4C4D; } -.vs-dark .monaco-menu .monaco-action-bar.vertical .action-item:hover:not(.disabled) { - background-color: #3A3A3A; -} - .vs-dark .context-view.monaco-menu-container { box-shadow: 0 2px 8px #000; color: #BBB; @@ -141,16 +144,7 @@ box-shadow: none; } -.hc-black .monaco-menu .monaco-action-bar.vertical .action-item { - border: 1px solid transparent; /* prevents jumpig behaviour on hover or focus */ -} - .hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused { background: none; border: 1px dotted #f38518; -} - -.hc-black .monaco-menu .monaco-action-bar.vertical .action-item:hover:not(.disabled) { - background: none; - border: 1px dashed #f38518; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 2eb053fbdc0..a2cb23d310e 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -6,14 +6,16 @@ 'use strict'; import 'vs/css!./menu'; +import * as nls from 'vs/nls'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IActionRunner, IAction, Action } from 'vs/base/common/actions'; -import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions, BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; 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 { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { $, Builder } from 'vs/base/browser/builder'; +import { RunOnceScheduler } from 'vs/base/common/async'; export interface IMenuOptions { context?: any; @@ -65,7 +67,7 @@ export class Menu { this.actionBar.push(actions, { icon: true, label: true, isMenu: true }); } - private doGetActionItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): ActionItem { + private doGetActionItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): BaseActionItem { if (action instanceof Separator) { return new ActionItem(options.context, action, { icon: true }); } else if (action instanceof SubmenuAction) { @@ -110,42 +112,128 @@ export class Menu { } } -class MenuActionItem extends ActionItem { +class MenuActionItem extends BaseActionItem { static MNEMONIC_REGEX: RegExp = /&&(.)/g; + protected $e: Builder; + protected $label: Builder; + protected options: IActionItemOptions; + private cssClass: string; + constructor(ctx: any, action: IAction, options: IActionItemOptions = {}) { options.isMenu = true; super(action, action, options); - } - private _addMnemonic(action: IAction, actionItemElement: HTMLElement): void { - let matches = MenuActionItem.MNEMONIC_REGEX.exec(action.label); - if (matches && matches.length === 2) { - let mnemonic = matches[1]; - - let ariaLabel = action.label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic); - - actionItemElement.accessKey = mnemonic.toLocaleLowerCase(); - this.$e.attr('aria-label', ariaLabel); - } else { - this.$e.attr('aria-label', action.label); - } + this.options = options; + this.options.icon = options.icon !== undefined ? options.icon : false; + this.options.label = options.label !== undefined ? options.label : true; + this.cssClass = ''; } public render(container: HTMLElement): void { super.render(container); - this._addMnemonic(this.getAction(), container); - this.$e.attr('role', 'menuitem'); + this.$e = $('a.action-menu-item').appendTo(this.builder); + if (this._action.id === Separator.ID) { + // A separator is a presentation item + this.$e.attr({ role: 'presentation' }); + } else { + this.$e.attr({ role: 'menuitem' }); + } + + this.$label = $('span.action-label').appendTo(this.$e); + + if (this.options.label && this.options.keybinding) { + $('span.keybinding').text(this.options.keybinding).appendTo(this.$e); + } + + this._updateClass(); + this._updateLabel(); + this._updateTooltip(); + this._updateEnabled(); + this._updateChecked(); + } + + public focus(): void { + super.focus(); + this.$e.domFocus(); } public _updateLabel(): void { if (this.options.label) { let label = this.getAction().label; - if (label && this.options.isMenu) { + if (label) { + let matches = MenuActionItem.MNEMONIC_REGEX.exec(label); + if (matches && matches.length === 2) { + let mnemonic = matches[1]; + + let ariaLabel = label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic); + + this.$e.getHTMLElement().accessKey = mnemonic.toLocaleLowerCase(); + this.$label.attr('aria-label', ariaLabel); + } else { + this.$label.attr('aria-label', label); + } + label = label.replace(MenuActionItem.MNEMONIC_REGEX, '$1\u0332'); } - this.$e.text(label); + + this.$label.text(label); + } + } + + public _updateTooltip(): void { + let title: string = null; + + if (this.getAction().tooltip) { + title = this.getAction().tooltip; + + } else if (!this.options.label && this.getAction().label && this.options.icon) { + title = this.getAction().label; + + if (this.options.keybinding) { + title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding); + } + } + + if (title) { + this.$e.attr({ title: title }); + } + } + + public _updateClass(): void { + if (this.cssClass) { + this.$e.removeClass(this.cssClass); + } + if (this.options.icon) { + this.cssClass = this.getAction().class; + this.$label.addClass('icon'); + if (this.cssClass) { + this.$label.addClass(this.cssClass); + } + this._updateEnabled(); + } else { + this.$label.removeClass('icon'); + } + } + + public _updateEnabled(): void { + if (this.getAction().enabled) { + this.builder.removeClass('disabled'); + this.$e.removeClass('disabled'); + this.$e.attr({ tabindex: 0 }); + } else { + this.builder.addClass('disabled'); + this.$e.addClass('disabled'); + removeTabIndexAndUpdateFocus(this.$e.getHTMLElement()); + } + } + + public _updateChecked(): void { + if (this.getAction().checked) { + this.$label.addClass('checked'); + } else { + this.$label.removeClass('checked'); } } } @@ -154,6 +242,8 @@ class SubmenuActionItem extends MenuActionItem { private mysubmenu: Menu; private submenuContainer: Builder; private mouseOver: boolean; + private showScheduler: RunOnceScheduler; + private hideScheduler: RunOnceScheduler; constructor( action: IAction, @@ -162,15 +252,28 @@ class SubmenuActionItem extends MenuActionItem { private submenuOptions?: IMenuOptions ) { super(action, action, { label: true, isMenu: true }); + + this.showScheduler = new RunOnceScheduler(() => { + if (this.mouseOver) { + this.cleanupExistingSubmenu(false); + this.createSubmenu(); + } + }, 250); + + this.hideScheduler = new RunOnceScheduler(() => { + if (!this.mouseOver && this.parentData.submenu === this.mysubmenu) { + this.parentData.parent.focus(); + this.cleanupExistingSubmenu(true); + } + }, 750); } public render(container: HTMLElement): void { super.render(container); - this.builder = $(container); - $(this.builder).addClass('monaco-submenu-item'); - $('span.submenu-indicator').text('\u25B6').appendTo(this.builder); - this.$e.attr('role', 'menu'); + this.$e.addClass('monaco-submenu-item'); + this.$e.attr('aria-haspopup', 'true'); + $('span.submenu-indicator').text('\u25B6').appendTo(this.$e); $(this.builder).on(EventType.KEY_UP, (e) => { let event = new StandardKeyboardEvent(e as KeyboardEvent); @@ -192,33 +295,22 @@ class SubmenuActionItem extends MenuActionItem { if (!this.mouseOver) { this.mouseOver = true; - setTimeout(() => { - if (this.mouseOver) { - this.cleanupExistingSubmenu(false); - this.createSubmenu(); - } - }, 250); - + this.showScheduler.schedule(); } }); - $(this.builder).on(EventType.MOUSE_LEAVE, (e) => { this.mouseOver = false; - setTimeout(() => { - if (!this.mouseOver && this.parentData.submenu === this.mysubmenu) { - this.parentData.parent.focus(); - this.cleanupExistingSubmenu(true); - } - - }, 750); + this.hideScheduler.schedule(); }); } public onClick(e: EventLike) { // stop clicking from trying to run an action EventHelper.stop(e, true); + + this.createSubmenu(); } private cleanupExistingSubmenu(force: boolean) { @@ -267,12 +359,17 @@ class SubmenuActionItem extends MenuActionItem { this.parentData.submenu.focus(); this.mysubmenu = this.parentData.submenu; + } else { + this.parentData.submenu.focus(); } } public dispose() { super.dispose(); + this.hideScheduler.dispose(); + this.showScheduler.dispose(); + if (this.mysubmenu) { this.mysubmenu.dispose(); this.mysubmenu = null; diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 5aafe0bd8a0..3d2f82a5cc7 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -163,6 +163,8 @@ export abstract class AbstractScrollableElement extends Widget { private readonly _hideTimeout: TimeoutTimer; private _shouldRender: boolean; + private _revealOnScroll: boolean; + private readonly _onScroll = this._register(new Emitter()); public readonly onScroll: Event = this._onScroll.event; @@ -221,6 +223,8 @@ export abstract class AbstractScrollableElement extends Widget { this._mouseIsOver = false; this._shouldRender = true; + + this._revealOnScroll = true; } public dispose(): void { @@ -286,6 +290,10 @@ export abstract class AbstractScrollableElement extends Widget { } } + public setRevealOnScroll(value: boolean) { + this._revealOnScroll = value; + } + // -------------------- mouse wheel scrolling -------------------- private _setListeningToMouseWheel(shouldListen: boolean): void { @@ -382,7 +390,9 @@ export abstract class AbstractScrollableElement extends Widget { this._shouldRender = true; } - this._reveal(); + if (this._revealOnScroll) { + this._reveal(); + } if (!this._options.lazyRender) { this._render(); diff --git a/src/vs/base/browser/ui/selectBox/selectBox.ts b/src/vs/base/browser/ui/selectBox/selectBox.ts index af1752d992e..5c7a28190a3 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.ts +++ b/src/vs/base/browser/ui/selectBox/selectBox.ts @@ -23,6 +23,7 @@ export interface ISelectBoxDelegate { readonly onDidSelect: Event; setOptions(options: string[], selected?: number, disabled?: number): void; select(index: number): void; + setAriaLabel(label: string); focus(): void; blur(): void; dispose(): void; @@ -34,6 +35,7 @@ export interface ISelectBoxDelegate { } export interface ISelectBoxOptions { + ariaLabel?: string; minBottomMargin?: number; } @@ -67,7 +69,7 @@ export class SelectBox extends Widget implements ISelectBoxDelegate { // Instantiate select implementation based on platform if (isMacintosh) { - this.selectBoxDelegate = new SelectBoxNative(options, selected, styles); + this.selectBoxDelegate = new SelectBoxNative(options, selected, styles, selectBoxOptions); } else { this.selectBoxDelegate = new SelectBoxList(options, selected, contextViewProvider, styles, selectBoxOptions); } @@ -89,6 +91,10 @@ export class SelectBox extends Widget implements ISelectBoxDelegate { this.selectBoxDelegate.select(index); } + public setAriaLabel(label: string): void { + this.selectBoxDelegate.setAriaLabel(label); + } + public focus(): void { this.selectBoxDelegate.focus(); } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index 362b7beab18..dcf29613a97 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -3,6 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* Use custom CSS vars to expose padding into parent select for padding calculation */ +.monaco-select-box-dropdown-padding { + --dropdown-padding-top: 1px; + --dropdown-padding-bottom: 1px; +} + +.hc-black .monaco-select-box-dropdown-padding { + --dropdown-padding-top: 3px; + --dropdown-padding-bottom: 4px; +} + .monaco-select-box-dropdown-container { display: none; } @@ -18,8 +29,8 @@ .monaco-select-box-dropdown-container > .select-box-dropdown-list-container { flex: 0 0 auto; align-self: flex-start; - padding-bottom: 1px; - padding-top: 1px; + padding-top: var(--dropdown-padding-top); + padding-bottom: var(--dropdown-padding-bottom); padding-left: 1px; padding-right: 1px; width: 100%; @@ -32,8 +43,8 @@ } .hc-black .monaco-select-box-dropdown-container > .select-box-dropdown-list-container { - padding-bottom: 4px; - padding-top: 3px; + padding-top: var(--dropdown-padding-top); + padding-bottom: var(--dropdown-padding-bottom); } .monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-text { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index fa9bcedd7e3..2efed5c5dcf 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -12,9 +12,9 @@ import { KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; -import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { IContextViewProvider, AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { domEvent } from 'vs/base/browser/event'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; @@ -67,14 +67,20 @@ class SelectListRenderer implements IRenderer { +export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate { private static readonly DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32; + private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 42; + private static readonly DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3; private _isVisible: boolean; private selectBoxOptions: ISelectBoxOptions; @@ -93,6 +99,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate(); + this.styles = styles; this.registerListeners(); @@ -133,7 +146,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate this.selectElement, render: (container: HTMLElement) => this.renderSelectDropDown(container), @@ -374,8 +402,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate { dom.toggleClass(this.selectDropDownContainer, 'visible', false); dom.toggleClass(this.selectElement, 'synthetic-focus', false); - } + }, + anchorPosition: this._dropDownPosition }); + + // Track initial selection the case user escape, blur this._currentSelection = this.selected; } @@ -397,40 +428,74 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate container.removeChild(this.selectDropDownContainer) // remove to take out the CSS rules we add + dispose: () => { + // contextView will dispose itself if moving from one View to another + try { + container.removeChild(this.selectDropDownContainer); // remove to take out the CSS rules we add + } + catch (error) { + // Ignore, removed already by change of focus + } + } }; } - private layoutSelectDropDown() { + private layoutSelectDropDown(preLayoutPosition?: boolean) { // Layout ContextView drop down select list and container - // Have to manage our vertical overflow, sizing - // Need to be visible to measure + // Have to manage our vertical overflow, sizing, position below or above + // Position has to be determined and set prior to contextView instantiation - dom.toggleClass(this.selectDropDownContainer, 'visible', true); - - const selectWidth = dom.getTotalWidth(this.selectElement); - const selectPosition = dom.getDomNodePagePosition(this.selectElement); - - // Set container height to max from select bottom to margin (default/minBottomMargin) - let maxSelectDropDownHeight = (window.innerHeight - selectPosition.top - selectPosition.height - this.selectBoxOptions.minBottomMargin); - - if (maxSelectDropDownHeight < 0) { - maxSelectDropDownHeight = 0; - } - - // SetUp list dimensions and layout - account for container padding if (this.selectList) { + + const selectPosition = dom.getDomNodePagePosition(this.selectElement); + const styles = getComputedStyle(this.selectElement); + const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom')); + let maxSelectDropDownHeight = 0; + maxSelectDropDownHeight = (window.innerHeight - selectPosition.top - selectPosition.height - this.selectBoxOptions.minBottomMargin); + this.selectList.layout(); let listHeight = this.selectList.contentHeight; - const listContainerHeight = dom.getTotalHeight(this.selectDropDownListContainer); - const totalVerticalListPadding = listContainerHeight - listHeight; - // Always show complete list items - never more than Max available vertical height - if (listContainerHeight > maxSelectDropDownHeight) { - listHeight = ((Math.floor((maxSelectDropDownHeight - totalVerticalListPadding) / this.getHeight())) * this.getHeight()); + // If we are only doing pre-layout check/adjust position only + // Calculate vertical space available, flip up if insufficient + // Use reflected padding on parent select, ContextView style properties not available before DOM attachment + if (preLayoutPosition) { + + // Always show complete list items - never more than Max available vertical height + if (listHeight + verticalPadding > maxSelectDropDownHeight) { + const maxVisibleOptions = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight()))); + + // Check if we can at least show min items otherwise flip above + if (maxVisibleOptions < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS) { + this._dropDownPosition = AnchorPosition.ABOVE; + } else { + this._dropDownPosition = AnchorPosition.BELOW; + } + } + // Do full layout on showSelectDropDown only + return; } + // Make visible to enable measurements + dom.toggleClass(this.selectDropDownContainer, 'visible', true); + + // SetUp list dimensions and layout - account for container padding + // Use position to check above or below available space + if (this._dropDownPosition === AnchorPosition.BELOW) { + // Set container height to max from select bottom to margin (default/minBottomMargin) + if (listHeight + verticalPadding > maxSelectDropDownHeight) { + listHeight = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())) * this.getHeight()); + } + } else { + // Set container height to max from select top to margin (default/minTopMargin) + maxSelectDropDownHeight = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN); + if (listHeight + verticalPadding > maxSelectDropDownHeight) { + listHeight = ((Math.floor((maxSelectDropDownHeight - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN) / this.getHeight())) * this.getHeight()); + } + } + + // Set adjusted list height and relayout this.selectList.layout(listHeight); this.selectList.domFocus(); @@ -441,13 +506,14 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate this.onMouseUp(e), this, this.toDispose); this.toDispose.push(this.selectList.onDidBlur(e => this.onListBlur())); + + this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true'); } // List methods - // List mouse controller - active exit, select option, fire onDidSelect, return focus to parent select + // List mouse controller - active exit, select option, fire onDidSelect if change, return focus to parent select private onMouseUp(e: MouseEvent): void { + dom.EventHelper.stop(e); + // Check our mouse event is on an option (not scrollbar) if (!e.toElement.classList.contains('option-text')) { return; @@ -542,59 +613,58 @@ export class SelectBoxList implements ISelectBoxDelegate, IDelegate= 0) { + if (this.selected !== this._currentSelection) { + // Reset selected to current if no change this.select(this._currentSelection); } - this._onDidSelect.fire({ - index: this.selectElement.selectedIndex, - selected: this.selectElement.title - }); - this.hideSelectDropDown(false); } // List keyboard controller - // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect + + // List exit - active - hide ContextView dropdown, reset selection, return focus to parent select private onEscape(e: StandardKeyboardEvent): void { dom.EventHelper.stop(e); + + // Reset selection to value when opened this.select(this._currentSelection); - this.hideSelectDropDown(true); - - this._onDidSelect.fire({ - index: this.selectElement.selectedIndex, - selected: this.selectElement.title - }); } - // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect + // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect if change private onEnter(e: StandardKeyboardEvent): void { dom.EventHelper.stop(e); - // Reset current selection - this._currentSelection = -1; + // Only fire if selection change + if (this.selected !== this._currentSelection) { + this._currentSelection = this.selected; + this._onDidSelect.fire({ + index: this.selectElement.selectedIndex, + selected: this.selectElement.title + }); + } this.hideSelectDropDown(true); - this._onDidSelect.fire({ - index: this.selectElement.selectedIndex, - selected: this.selectElement.title - }); } // List navigation - have to handle a disabled option (jump over) diff --git a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts index c16bd47e0f3..ecd7fcca737 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts @@ -8,31 +8,36 @@ import { Event, Emitter } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; -import { ISelectBoxDelegate, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; +import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; import { isMacintosh } from 'vs/base/common/platform'; export class SelectBoxNative implements ISelectBoxDelegate { private selectElement: HTMLSelectElement; + private selectBoxOptions: ISelectBoxOptions; private options: string[]; private selected: number; private readonly _onDidSelect: Emitter; private toDispose: IDisposable[]; private styles: ISelectBoxStyles; - constructor(options: string[], selected: number, styles: ISelectBoxStyles) { + constructor(options: string[], selected: number, styles: ISelectBoxStyles, selectBoxOptions?: ISelectBoxOptions) { this.toDispose = []; + this.selectBoxOptions = selectBoxOptions || Object.create(null); this.selectElement = document.createElement('select'); this.selectElement.className = 'monaco-select-box'; + if (typeof this.selectBoxOptions.ariaLabel === 'string') { + this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel); + } + this._onDidSelect = new Emitter(); this.styles = styles; this.registerListeners(); - this.setOptions(options, selected); } @@ -103,6 +108,11 @@ export class SelectBoxNative implements ISelectBoxDelegate { this.selectElement.title = this.options[this.selected]; } + public setAriaLabel(label: string): void { + this.selectBoxOptions.ariaLabel = label; + this.selectElement.setAttribute('aria-label', label); + } + public focus(): void { if (this.selectElement) { this.selectElement.focus(); diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 50e06012bba..02fcb82fa11 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -171,7 +171,7 @@ class ToggleMenuAction extends Action { private toggleDropdownMenu: () => void; constructor(toggleDropdownMenu: () => void) { - super(ToggleMenuAction.ID, nls.localize('more', "More"), null, true); + super(ToggleMenuAction.ID, nls.localize('moreActions', "More Actions..."), null, true); this.toggleDropdownMenu = toggleDropdownMenu; } diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 6cd72d3311f..18a45d2c74c 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -4,15 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./tree'; -import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } 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'; -import { IDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { TreeModel, ITreeNode, ITreeElement, getNodeLocation } from 'vs/base/browser/ui/tree/treeModel'; +import { Iterator, ISequence } from 'vs/base/common/iterator'; +import { IVirtualDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { append, $ } from 'vs/base/browser/dom'; import { Event, Relay, chain } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { tail2 } from 'vs/base/common/arrays'; function toTreeListOptions(options?: IListOptions): IListOptions> { if (!options) { @@ -44,9 +45,9 @@ function toTreeListOptions(options?: IListOptions): IListOptions implements IDelegate> { +class TreeDelegate implements IVirtualDelegate> { - constructor(private delegate: IDelegate) { } + constructor(private delegate: IVirtualDelegate) { } getHeight(element: ITreeNode): number { return this.delegate.getHeight(element.element); @@ -59,10 +60,17 @@ class TreeDelegate implements IDelegate> { interface ITreeListTemplateData { twistie: HTMLElement; - elementDisposable: IDisposable; templateData: T; } +function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { + if (node.children.length === 0 && !node.collapsible) { + twistie.innerText = ''; + } else { + twistie.innerText = node.collapsed ? '▹' : '◢'; + } +} + class TreeRenderer implements IRenderer, ITreeListTemplateData> { readonly templateId: string; @@ -83,21 +91,22 @@ class TreeRenderer implements IRenderer, ITreeLis const contents = append(el, $('.tl-contents')); const templateData = this.renderer.renderTemplate(contents); - return { twistie, elementDisposable: Disposable.None, templateData }; + return { twistie, templateData }; } renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { - templateData.elementDisposable.dispose(); - this.renderedNodes.set(node, templateData); - templateData.elementDisposable = toDisposable(() => this.renderedNodes.delete(node)); - templateData.twistie.innerText = node.children.length === 0 ? '' : (node.collapsed ? '▹' : '◢'); templateData.twistie.style.width = `${10 + node.depth * 10}px`; + renderTwistie(node, templateData.twistie); this.renderer.renderElement(node.element, index, templateData.templateData); } + disposeElement(node: ITreeNode): void { + this.renderedNodes.delete(node); + } + disposeTemplate(templateData: ITreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } @@ -109,7 +118,7 @@ class TreeRenderer implements IRenderer, ITreeLis return; } - templateData.twistie.innerText = node.children.length === 0 ? '' : (node.collapsed ? '▹' : '◢'); + renderTwistie(node, templateData.twistie); } dispose(): void { @@ -118,21 +127,12 @@ class TreeRenderer implements IRenderer, ITreeLis } } -function getLocation(node: ITreeNode): number[] { - const location = []; - - while (node.parent) { - location.push(node.parent.children.indexOf(node)); - node = node.parent; - } - - return location.reverse(); -} - function isInputElement(e: HTMLElement): boolean { return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; } +export interface ITreeOptions extends IListOptions { } + export class Tree implements IDisposable { private view: List>; @@ -141,9 +141,9 @@ export class Tree implements IDisposable { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IRenderer[], - options?: IListOptions + options?: ITreeOptions ) { const treeDelegate = new TreeDelegate(delegate); @@ -168,13 +168,13 @@ export class Tree implements IDisposable { onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); } - splice(location: number[], deleteCount: number, toInsert: IIterator> = empty()): IIterator> { + splice(location: number[], deleteCount: number, toInsert: ISequence> = Iterator.empty()): Iterator> { return this.model.splice(location, deleteCount, toInsert); } private onMouseClick(e: IListMouseEvent>): void { const node = e.element; - const location = getLocation(node); + const location = getNodeLocation(node); this.model.toggleCollapsed(location); } @@ -190,12 +190,17 @@ export class Tree implements IDisposable { } const node = nodes[0]; - const location = getLocation(node); - const didCollapse = this.model.setCollapsed(location, true); + const location = getNodeLocation(node); + const didChange = this.model.setCollapsed(location, true); - if (!didCollapse) { - console.log('should focus parent'); - // this.view.setFocus([]); + if (!didChange) { + if (location.length === 1) { + return; + } + + const [parentLocation] = tail2(location); + const parentListIndex = this.model.getListIndex(parentLocation); + this.view.setFocus([parentListIndex]); } } @@ -210,8 +215,17 @@ export class Tree implements IDisposable { } const node = nodes[0]; - const location = getLocation(node); - this.model.setCollapsed(location, false); + const location = getNodeLocation(node); + const didChange = this.model.setCollapsed(location, false); + + if (!didChange) { + if (node.children.length === 0) { + return; + } + + const [focusedIndex] = this.view.getFocus(); + this.view.setFocus([focusedIndex + 1]); + } } private onSpace(e: StandardKeyboardEvent): void { @@ -225,7 +239,7 @@ export class Tree implements IDisposable { } const node = nodes[0]; - const location = getLocation(node); + const location = getNodeLocation(node); this.model.toggleCollapsed(location); } diff --git a/src/vs/base/browser/ui/tree/treeModel.ts b/src/vs/base/browser/ui/tree/treeModel.ts index 7acf8a90a4b..1a30ff92292 100644 --- a/src/vs/base/browser/ui/tree/treeModel.ts +++ b/src/vs/base/browser/ui/tree/treeModel.ts @@ -6,13 +6,13 @@ 'use strict'; 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 { Iterator, ISequence } from 'vs/base/common/iterator'; import { Emitter, Event } from 'vs/base/common/event'; export interface ITreeElement { readonly element: T; - readonly children?: IIterator> | ITreeElement[]; + readonly children?: Iterator> | ITreeElement[]; + readonly collapsible?: boolean; readonly collapsed?: boolean; } @@ -21,6 +21,7 @@ export interface ITreeNode { readonly element: T; readonly children: IMutableTreeNode[]; readonly depth: number; + readonly collapsible: boolean; readonly collapsed: boolean; readonly visibleCount: number; } @@ -50,11 +51,11 @@ function getVisibleNodes(nodes: IMutableTreeNode[], result: ITreeNode[] return result; } -function getTreeElementIterator(elements: IIterator> | ITreeElement[] | undefined): IIterator> { +function getTreeElementIterator(elements: Iterator> | ITreeElement[] | undefined): Iterator> { if (!elements) { - return empty(); + return Iterator.empty(); } else if (Array.isArray(elements)) { - return iter(elements); + return Iterator.iterate(elements); } else { return elements; } @@ -62,15 +63,16 @@ function getTreeElementIterator(elements: IIterator> | ITreeE function treeElementToNode(treeElement: ITreeElement, parent: IMutableTreeNode, visible: boolean, treeListElements: ITreeNode[]): IMutableTreeNode { const depth = parent.depth + 1; - const { element, collapsed } = treeElement; - const node = { parent, element, children: [], depth, collapsed: !!collapsed, visibleCount: 0 }; + const { element, collapsible, collapsed } = treeElement; + const node = { parent, element, children: [], depth, collapsible: !!collapsible, collapsed: !!collapsed, visibleCount: 0 }; if (visible) { treeListElements.push(node); } const children = getTreeElementIterator(treeElement.children); - node.children = collect(map(children, el => treeElementToNode(el, node, visible && !treeElement.collapsed, treeListElements))); + node.children = Iterator.collect(Iterator.map(children, el => treeElementToNode(el, node, visible && !treeElement.collapsed, treeListElements))); + node.collapsible = node.collapsible || node.children.length > 0; node.visibleCount = 1 + getVisibleCount(node.children); return node; @@ -78,11 +80,22 @@ function treeElementToNode(treeElement: ITreeElement, parent: IMutableTree function treeNodeToElement(node: IMutableTreeNode): ITreeElement { const { element, collapsed } = node; - const children = map(iter(node.children), treeNodeToElement); + const children = Iterator.map(Iterator.iterate(node.children), treeNodeToElement); return { element, children, collapsed }; } +export function getNodeLocation(node: ITreeNode): number[] { + const location = []; + + while (node.parent) { + location.push(node.parent.children.indexOf(node)); + node = node.parent; + } + + return location.reverse(); +} + export class TreeModel { private root: IMutableTreeNode = { @@ -90,6 +103,7 @@ export class TreeModel { element: undefined, children: [], depth: 0, + collapsible: false, collapsed: false, visibleCount: 1 }; @@ -99,7 +113,7 @@ export class TreeModel { constructor(private list: ISpliceable>) { } - splice(location: number[], deleteCount: number, toInsert?: IIterator> | ITreeElement[]): IIterator> { + splice(location: number[], deleteCount: number, toInsert?: ISequence>): Iterator> { if (location.length === 0) { throw new Error('Invalid tree location'); } @@ -107,8 +121,9 @@ export class TreeModel { const { parentNode, listIndex, visible } = this.findParentNode(location); 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 nodesToInsert = Iterator.collect(Iterator.map(elementsToInsert, el => treeElementToNode(el, parentNode, visible, treeListElementsToInsert))); + const lastIndex = location[location.length - 1]; + const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert); const visibleDeleteCount = getVisibleCount(deletedNodes); parentNode.visibleCount += getVisibleCount(nodesToInsert) - visibleDeleteCount; @@ -117,7 +132,11 @@ export class TreeModel { this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert); } - return map(iter(deletedNodes), treeNodeToElement); + return Iterator.map(Iterator.iterate(deletedNodes), treeNodeToElement); + } + + getListIndex(location: number[]): number { + return this.findNode(location).listIndex; } setCollapsed(location: number[], collapsed: boolean): boolean { @@ -131,6 +150,10 @@ export class TreeModel { private _setCollapsed(location: number[], collapsed?: boolean | undefined): boolean { const { node, listIndex, visible } = this.findNode(location); + if (!node.collapsible) { + return false; + } + if (typeof collapsed === 'undefined') { collapsed = !node.collapsed; } @@ -142,6 +165,8 @@ export class TreeModel { node.collapsed = collapsed; if (visible) { + this._onDidChangeCollapseState.fire(node); + let visibleCountDiff: number; if (collapsed) { @@ -162,8 +187,6 @@ export class TreeModel { mutableNode.visibleCount += visibleCountDiff; mutableNode = mutableNode.parent; } - - this._onDidChangeCollapseState.fire(node); } return true; @@ -175,7 +198,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..7b2d7e3b563 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[]; @@ -470,14 +474,19 @@ export function arrayInsert(target: T[], insertIndex: number, insertArr: T[]) * Uses Fisher-Yates shuffle to shuffle the given array * @param array */ -export function shuffle(array: T[]): void { - var i = 0 - , j = 0 - , temp = null; +export function shuffle(array: T[], seed?: number): void { + // Seeded random number generator in JS. Modified from: + // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript + const random = () => { + var x = Math.sin(seed++) * 179426549; // throw away most significant digits and reduce any potential bias + return x - Math.floor(x); + }; - for (i = array.length - 1; i > 0; i -= 1) { - j = Math.floor(Math.random() * (i + 1)); - temp = array[i]; + const rand = typeof seed === 'number' ? random : Math.random; + + for (let i = array.length - 1; i > 0; i -= 1) { + let j = Math.floor(rand() * (i + 1)); + let temp = array[i]; array[i] = array[j]; array[j] = temp; } diff --git a/src/vs/base/common/decorators.ts b/src/vs/base/common/decorators.ts index 719407fe245..3fac80e7c15 100644 --- a/src/vs/base/common/decorators.ts +++ b/src/vs/base/common/decorators.ts @@ -69,19 +69,23 @@ export interface IDebouceReducer { export function debounce(delay: number, reducer?: IDebouceReducer, initialValueProvider?: () => T): Function { return createDecorator((fn, key) => { const timerKey = `$debounce$${key}`; - let result = initialValueProvider ? initialValueProvider() : void 0; + const resultKey = `$debounce$result$${key}`; return function (this: any, ...args: any[]) { + if (!this[resultKey]) { + this[resultKey] = initialValueProvider ? initialValueProvider() : void 0; + } + clearTimeout(this[timerKey]); if (reducer) { - result = reducer(result, ...args); - args = [result]; + this[resultKey] = reducer(this[resultKey], ...args); + args = [this[resultKey]]; } this[timerKey] = setTimeout(() => { fn.apply(this, args); - result = initialValueProvider ? initialValueProvider() : void 0; + this[resultKey] = initialValueProvider ? initialValueProvider() : void 0; }, delay); }; }); diff --git a/src/vs/base/common/diagnostics.ts b/src/vs/base/common/diagnostics.ts deleted file mode 100644 index 7ad7daa18d4..00000000000 --- a/src/vs/base/common/diagnostics.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 Platform from 'vs/base/common/platform'; - -/** - * To enable diagnostics, open a browser console and type: window.Monaco.Diagnostics. = true. - * Then trigger an action that will write to diagnostics to see all cached output from the past. - */ - -const globals = Platform.globals; -if (!globals.Monaco) { - globals.Monaco = {}; -} -globals.Monaco.Diagnostics = {}; - -const switches = globals.Monaco.Diagnostics; -const map = new Map(); -const data: any[] = []; - -function fifo(array: any[], size: number) { - while (array.length > size) { - array.shift(); - } -} - -export function register(what: string, fn: Function): (...args: any[]) => void { - - let disable = true; // Otherwise we have unreachable code. - if (disable) { - return () => { - // Intentional empty, disable for now because it is leaking memory - }; - } - - // register switch - const flag = switches[what] || false; - switches[what] = flag; - - // register function - const tracers = map.get(what) || []; - tracers.push(fn); - map.set(what, tracers); - - const result = function (...args: any[]) { - - let idx: number; - - if (switches[what] === true) { - // replay back-in-time functions - const allArgs = [arguments]; - idx = data.indexOf(fn); - if (idx !== -1) { - allArgs.unshift.apply(allArgs, data[idx + 1] || []); - data[idx + 1] = []; - } - - const doIt: () => void = function () { - const thisArguments = allArgs.shift(); - fn.apply(fn, thisArguments); - if (allArgs.length > 0) { - setTimeout(doIt, 500); - } - }; - doIt(); - - } else { - // know where to store - idx = data.indexOf(fn); - idx = idx !== -1 ? idx : data.length; - const dataIdx = idx + 1; - - // store arguments - const allargs = data[dataIdx] || []; - allargs.push(arguments); - fifo(allargs, 50); - - // store data - data[idx] = fn; - data[dataIdx] = allargs; - } - }; - - return result; -} diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index eb4c7cef1c1..37887a5318f 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -413,6 +413,7 @@ export interface IChainableEvent { filter(fn: (e: T) => boolean): IChainableEvent; latch(): IChainableEvent; on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; + once(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } export function mapEvent(event: Event, map: (i: I) => O): Event { @@ -454,6 +455,10 @@ class ChainableEvent implements IChainableEvent { on(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[]) { return this._event(listener, thisArgs, disposables); } + + once(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[]) { + return once(this._event)(listener, thisArgs, disposables); + } } export function chain(event: Event): IChainableEvent { diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index cd8d81b84f3..93365a9fb6a 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -247,8 +247,8 @@ const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else export type ParsedPattern = (path: string, basename?: string) => boolean; -// The ParsedExpression returns a Promise iff siblingsFn returns a Promise. -export type ParsedExpression = (path: string, basename?: string, siblingsFn?: () => string[] | TPromise) => string | TPromise /* the matching pattern */; +// The ParsedExpression returns a Promise iff hasSibling returns a Promise. +export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise) => string | TPromise /* the matching pattern */; export interface IGlobOptions { /** @@ -264,9 +264,8 @@ interface ParsedStringPattern { allBasenames?: string[]; allPaths?: string[]; } -type SiblingsPattern = { siblings: string[], name: string }; interface ParsedExpressionPattern { - (path: string, basename: string, siblingsPatternFn: () => SiblingsPattern | TPromise): string | TPromise /* the matching pattern */; + (path: string, basename: string, name: string, hasSibling: (name: string) => boolean | TPromise): string | TPromise /* the matching pattern */; requiresSiblings?: boolean; allBasenames?: string[]; allPaths?: string[]; @@ -436,13 +435,13 @@ function toRegExp(pattern: string): ParsedStringPattern { * - character ranges (using [...]) */ export function match(pattern: string | IRelativePattern, path: string): boolean; -export function match(expression: IExpression, path: string, siblingsFn?: () => string[]): string /* the matching pattern */; -export function match(arg1: string | IExpression | IRelativePattern, path: string, siblingsFn?: () => string[]): any { +export function match(expression: IExpression, path: string, hasSibling?: (name: string) => boolean): string /* the matching pattern */; +export function match(arg1: string | IExpression | IRelativePattern, path: string, hasSibling?: (name: string) => boolean): any { if (!arg1 || !path) { return false; } - return parse(arg1)(path, undefined, siblingsFn); + return parse(arg1)(path, undefined, hasSibling); } /** @@ -482,6 +481,44 @@ export function parse(arg1: string | IExpression | IRelativePattern, options: IG return parsedExpression(arg1, options); } +export function hasSiblingPromiseFn(siblingsFn?: () => TPromise) { + if (!siblingsFn) { + return undefined; + } + + let siblings: TPromise>; + return (name: string) => { + if (!siblings) { + siblings = (siblingsFn() || TPromise.as([])) + .then(list => list ? listToMap(list) : {}); + } + return siblings.then(map => !!map[name]); + }; +} + +export function hasSiblingFn(siblingsFn?: () => string[]) { + if (!siblingsFn) { + return undefined; + } + + let siblings: Record; + return (name: string) => { + if (!siblings) { + const list = siblingsFn(); + siblings = list ? listToMap(list) : {}; + } + return !!siblings[name]; + }; +} + +function listToMap(list: string[]) { + const map: Record = {}; + for (const key of list) { + map[key] = true; + } + return map; +} + export function isRelativePattern(obj: any): obj is IRelativePattern { const rp = obj as IRelativePattern; @@ -493,8 +530,8 @@ export function isRelativePattern(obj: any): obj is IRelativePattern { */ export function parseToAsync(expression: IExpression, options?: IGlobOptions): ParsedExpression { const parsedExpression = parse(expression, options); - return (path: string, basename?: string, siblingsFn?: () => string[] | TPromise): string | TPromise => { - const result = parsedExpression(path, basename, siblingsFn); + return (path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): string | TPromise => { + const result = parsedExpression(path, basename, hasSibling); return result instanceof TPromise ? result : TPromise.as(result); }; } @@ -522,7 +559,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return parsedPatterns[0]; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string, siblingsFn?: () => string[]) { + const resultExpression: ParsedStringPattern = function (path: string, basename: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { // Pattern matches path const result = (parsedPatterns[i])(path, basename); @@ -547,38 +584,21 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return resultExpression; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string, siblingsFn?: () => string[] | TPromise) { - let siblingsPattern: SiblingsPattern | TPromise; - let siblingsResolved = !siblingsFn; - - function siblingsToSiblingsPattern(siblings: string[]) { - if (siblings && siblings.length) { - if (!basename) { - basename = paths.basename(path); - } - const name = basename.substr(0, basename.length - paths.extname(path).length); - return { siblings, name }; - } - - return undefined; - } - - function siblingsPatternFn() { - // Resolve siblings only once - if (!siblingsResolved) { - siblingsResolved = true; - const siblings = siblingsFn(); - siblingsPattern = TPromise.is(siblings) ? - siblings.then(siblingsToSiblingsPattern) : - siblingsToSiblingsPattern(siblings); - } - - return siblingsPattern; - } + const resultExpression: ParsedStringPattern = function (path: string, basename: string, hasSibling?: (name: string) => boolean | TPromise) { + let name: string; for (let i = 0, n = parsedPatterns.length; i < n; i++) { // Pattern matches path - const result = (parsedPatterns[i])(path, basename, siblingsPatternFn); + const parsedPattern = (parsedPatterns[i]); + if (parsedPattern.requiresSiblings && hasSibling) { + if (!basename) { + basename = paths.basename(path); + } + if (!name) { + name = basename.substr(0, basename.length - paths.extname(path).length); + } + } + const result = parsedPattern(path, basename, name, hasSibling); if (result) { return result; } @@ -619,28 +639,16 @@ function parseExpressionPattern(pattern: string, value: any, options: IGlobOptio if (value) { const when = (value).when; if (typeof when === 'string') { - const siblingsPatternToMatchingPattern = (siblingsPattern: SiblingsPattern): string => { - let clausePattern = when.replace('$(basename)', siblingsPattern.name); - if (siblingsPattern.siblings.indexOf(clausePattern) !== -1) { - return pattern; - } else { - return null; // pattern does not match in the end because the when clause is not satisfied - } - }; - - const result: ParsedExpressionPattern = (path: string, basename: string, siblingsPatternFn: () => SiblingsPattern | TPromise) => { - if (!parsedPattern(path, basename)) { + const result: ParsedExpressionPattern = (path: string, basename: string, name: string, hasSibling: (name: string) => boolean | TPromise) => { + if (!hasSibling || !parsedPattern(path, basename)) { return null; } - const siblingsPattern = siblingsPatternFn(); - if (!siblingsPattern) { - return null; // pattern is malformed or we don't have siblings - } - - return TPromise.is(siblingsPattern) ? - siblingsPattern.then(siblingsPatternToMatchingPattern) : - siblingsPatternToMatchingPattern(siblingsPattern); + const clausePattern = when.replace('$(basename)', name); + const matched = hasSibling(clausePattern); + return TPromise.is(matched) ? + matched.then(m => m ? pattern : null) : + matched ? pattern : null; }; result.requiresSiblings = true; return result; diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 778819cadb8..9e6f3c57461 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -5,74 +5,86 @@ 'use strict'; -export interface IIteratorResult { +export interface IteratorResult { readonly done: boolean; readonly value: T | undefined; } -export interface IIterator { - next(): IIteratorResult; +export interface Iterator { + next(): IteratorResult; } -const _empty: IIterator = { - next() { - return { done: true, value: undefined }; +export module Iterator { + const _empty: Iterator = { + next() { + return { done: true, value: undefined }; + } + }; + + export function empty(): Iterator { + return _empty; } -}; -export function empty(): IIterator { - return _empty; -} + export function iterate(array: T[], index = 0, length = array.length): Iterator { + return { + next(): IteratorResult { + if (index >= length) { + return { done: true, value: undefined }; + } -export function iter(array: T[], index = 0, length = array.length): IIterator { - return { - next(): IIteratorResult { - if (index >= length) { - return { done: true, value: undefined }; + return { done: false, value: array[index++] }; } + }; + } - return { done: false, value: array[index++] }; - } - }; -} - -export function map(iterator: IIterator, fn: (t: T) => R): IIterator { - return { - next() { - const { done, value } = iterator.next(); - return { done, value: done ? undefined : fn(value) }; - } - }; -} - -export function filter(iterator: IIterator, fn: (t: T) => boolean): IIterator { - return { - next() { - while (true) { + export function map(iterator: Iterator, fn: (t: T) => R): Iterator { + return { + next() { const { done, value } = iterator.next(); + return { done, value: done ? undefined : fn(value) }; + } + }; + } - if (done) { - return { done, value: undefined }; - } + export function filter(iterator: Iterator, fn: (t: T) => boolean): Iterator { + return { + next() { + while (true) { + const { done, value } = iterator.next(); - if (fn(value)) { - return { done, value }; + if (done) { + return { done, value: undefined }; + } + + if (fn(value)) { + return { done, value }; + } } } - } - }; -} + }; + } -export function forEach(iterator: IIterator, fn: (t: T) => void): void { - for (let next = iterator.next(); !next.done; next = iterator.next()) { - fn(next.value); + export function forEach(iterator: Iterator, fn: (t: T) => void): void { + for (let next = iterator.next(); !next.done; next = iterator.next()) { + fn(next.value); + } + } + + export function collect(iterator: Iterator): T[] { + const result: T[] = []; + forEach(iterator, value => result.push(value)); + return result; } } -export function collect(iterator: IIterator): T[] { - const result: T[] = []; - forEach(iterator, value => result.push(value)); - return result; +export type ISequence = Iterator | T[]; + +export function getSequenceIterator(arg: Iterator | T[]): Iterator { + if (Array.isArray(arg)) { + return Iterator.iterate(arg); + } else { + return arg; + } } export interface INextIterator { diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index 8a4a058d898..15d9a56d269 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -84,7 +84,7 @@ export function getBaseLabel(resource: URI | string): string { resource = URI.file(resource); } - const base = pathsBasename(resource.fsPath) || resource.fsPath /* can be empty string if '/' is passed in */; + const base = pathsBasename(resource.path) || (resource.scheme === Schemas.file ? resource.fsPath : resource.path) /* can be empty string if '/' is passed in */; // convert c: => C: if (hasDriveLetter(base)) { diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts index d40fd2a2861..9f44422e99c 100644 --- a/src/vs/base/common/linkedList.ts +++ b/src/vs/base/common/linkedList.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; class Node { element: E; @@ -94,7 +94,7 @@ export class LinkedList { }; } - iterator(): IIterator { + iterator(): Iterator { let element = { done: undefined, value: undefined, diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index e234c796330..c39465b4991 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -7,7 +7,7 @@ import URI from 'vs/base/common/uri'; import { CharCode } from 'vs/base/common/charCode'; -import { IIterator } from './iterator'; +import { Iterator } from './iterator'; export function values(set: Set): V[]; export function values(map: Map): V[]; @@ -306,7 +306,7 @@ export class TernarySearchTree { return node && node.value || candidate; } - findSuperstr(key: string): IIterator { + findSuperstr(key: string): Iterator { let iter = this._iter.reset(key); let node = this._root; while (node) { @@ -333,7 +333,7 @@ export class TernarySearchTree { return undefined; } - private _nodeIterator(node: TernarySearchTreeNode): IIterator { + private _nodeIterator(node: TernarySearchTreeNode): Iterator { let res = { done: false, value: undefined diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index e7c4cb9d9c3..141b17598b2 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -7,6 +7,18 @@ import * as paths from 'vs/base/common/paths'; import uri from 'vs/base/common/uri'; import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; +import { isLinux } from 'vs/base/common/platform'; + +export function getComparisonKey(resource: uri): string { + return hasToIgnoreCase(resource) ? resource.toString().toLowerCase() : resource.toString(); +} + +export function hasToIgnoreCase(resource: uri): boolean { + // A file scheme resource is in the same platform as code, so ignore case for non linux platforms + // Resource can be from another platform. Lowering the case as an hack. Should come from File system provider + return resource.scheme === Schemas.file ? !isLinux : true; +} export function basenameOrAuthority(resource: uri): string { return paths.basename(resource.path) || resource.authority; diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index b943ced6ab1..23437967a6e 100644 --- a/src/vs/base/common/scrollable.ts +++ b/src/vs/base/common/scrollable.ts @@ -229,7 +229,7 @@ export class Scrollable extends Disposable { /** * Returns the final scroll position that the instance will have once the smooth scroll animation concludes. - * If no scroll animation is occuring, it will return the current scroll position instead. + * If no scroll animation is occurring, it will return the current scroll position instead. */ public getFutureScrollPosition(): IScrollPosition { if (this._smoothScrolling) { diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index eb01c197f74..ebe0dff4cc4 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as cp from 'child_process'; import { fork } from 'vs/base/node/stdFork'; import * as nls from 'vs/nls'; -import { PPromise, TPromise, TValueCallback, TProgressCallback, ErrorCallback } from 'vs/base/common/winjs.base'; +import { TPromise, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import * as Types from 'vs/base/common/types'; import { IStringDictionary } from 'vs/base/common/collections'; import URI from 'vs/base/common/uri'; @@ -19,6 +19,8 @@ import { LineDecoder } from 'vs/base/node/decoder'; import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode, Executable } from 'vs/base/common/processes'; export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode }; +export type TProgressCallback = (progress: T) => void; + export interface LineData { line: string; source: Source; @@ -152,18 +154,16 @@ export abstract class AbstractProcess { return 'other'; } - public start(): PPromise { + public start(pp: TProgressCallback): TPromise { if (Platform.isWindows && ((this.options && this.options.cwd && TPath.isUNC(this.options.cwd)) || !this.options && !this.options.cwd && TPath.isUNC(process.cwd()))) { return TPromise.wrapError(new Error(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on a UNC drive.'))); } return this.useExec().then((useExec) => { let cc: TValueCallback; let ee: ErrorCallback; - let pp: TProgressCallback; - let result = new PPromise((c, e, p) => { + let result = new TPromise((c, e) => { cc = c; ee = e; - pp = p; }); if (useExec) { 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 ed35efadee0..37555c26b07 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -94,8 +94,8 @@ export interface IChannelClient { * channels (each from a separate client) to pick from. */ export interface IClientRouter { - routeCall(command: string, arg: any): string; - routeEvent(event: string, arg: any): string; + routeCall(command: string, arg: any): TPromise; + routeEvent(event: string, arg: any): TPromise; } /** @@ -433,24 +433,20 @@ export class IPCServer implements IChannelServer, IRoutingChannelClient, IDispos getChannel(channelName: string, router: IClientRouter): T { const call = (command: string, arg: any) => { - const id = router.routeCall(command, arg); + const channelPromise = router.routeCall(command, arg) + .then(id => this.getClient(id)) + .then(client => client.getChannel(channelName)); - if (!id) { - return TPromise.wrapError(new Error('Client id should be provided')); - } - - return getDelayedChannel(this.getClient(id).then(client => client.getChannel(channelName))) + return getDelayedChannel(channelPromise) .call(command, arg); }; const listen = (event: string, arg: any) => { - const id = router.routeEvent(event, arg); + const channelPromise = router.routeEvent(event, arg) + .then(id => this.getClient(id)) + .then(client => client.getChannel(channelName)); - if (!id) { - return TPromise.wrapError(new Error('Client id should be provided')); - } - - return getDelayedChannel(this.getClient(id).then(client => client.getChannel(channelName))) + return getDelayedChannel(channelPromise) .listen(event, arg); }; @@ -462,6 +458,10 @@ export class IPCServer implements IChannelServer, IRoutingChannelClient, IDispos } private getClient(clientId: string): TPromise { + if (!clientId) { + return TPromise.wrapError(new Error('Client id should be provided')); + } + const client = this.channelClients[clientId]; if (client) { diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 0be4208d0e9..e82503e1eb0 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -12,6 +12,8 @@ import { IMessagePassingProtocol, ClientConnectionEvent, IPCServer, IPCClient } import { join } from 'path'; import { tmpdir } from 'os'; import { generateUuid } from 'vs/base/common/uuid'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { TimeoutTimer } from 'vs/base/common/async'; export function generateRandomPipeName(): string { const randomSuffix = generateUuid(); @@ -23,17 +25,28 @@ export function generateRandomPipeName(): string { } } -export class Protocol implements IMessagePassingProtocol { +export class Protocol implements IDisposable, IMessagePassingProtocol { - private static readonly _headerLen = 17; + private static readonly _headerLen = 5; + + private _isDisposed: boolean; + private _chunks: Buffer[]; + + private _firstChunkTimer: TimeoutTimer; + private _socketDataListener: (data: Buffer) => void; + private _socketEndListener: () => void; + private _socketCloseListener: () => void; private _onMessage = new Emitter(); - readonly onMessage: Event = this._onMessage.event; - constructor(private _socket: Socket, firstDataChunk?: Buffer) { + private _onClose = new Emitter(); + readonly onClose: Event = this._onClose.event; + + constructor(private _socket: Socket, firstDataChunk?: Buffer) { + this._isDisposed = false; + this._chunks = []; - let chunks: Buffer[] = []; let totalLength = 0; const state = { @@ -44,16 +57,16 @@ export class Protocol implements IMessagePassingProtocol { const acceptChunk = (data: Buffer) => { - chunks.push(data); + this._chunks.push(data); totalLength += data.length; while (totalLength > 0) { if (state.readHead) { - // expecting header -> read 17bytes for header + // expecting header -> read 5bytes for header // information: `bodyIsJson` and `bodyLen` if (totalLength >= Protocol._headerLen) { - const all = Buffer.concat(chunks); + const all = Buffer.concat(this._chunks); state.bodyIsJson = all.readInt8(0) === 1; state.bodyLen = all.readInt32BE(1); @@ -61,7 +74,7 @@ export class Protocol implements IMessagePassingProtocol { const rest = all.slice(Protocol._headerLen); totalLength = rest.length; - chunks = [rest]; + this._chunks = [rest]; } else { break; @@ -73,21 +86,27 @@ export class Protocol implements IMessagePassingProtocol { // the actual message or wait for more data if (totalLength >= state.bodyLen) { - const all = Buffer.concat(chunks); + const all = Buffer.concat(this._chunks); let message = all.toString('utf8', 0, state.bodyLen); if (state.bodyIsJson) { message = JSON.parse(message); } - this._onMessage.fire(message); + // ensure the public getBuffer returns a valid value if invoked from the event listeners const rest = all.slice(state.bodyLen); totalLength = rest.length; - chunks = [rest]; + this._chunks = [rest]; state.bodyIsJson = false; state.bodyLen = -1; state.readHead = true; + this._onMessage.fire(message); + + if (this._isDisposed) { + // check if an event listener lead to our disposal + break; + } } else { break; } @@ -103,14 +122,43 @@ export class Protocol implements IMessagePassingProtocol { } }; - _socket.on('data', (data: Buffer) => { + // Make sure to always handle the firstDataChunk if no more `data` event comes in + this._firstChunkTimer = new TimeoutTimer(); + this._firstChunkTimer.setIfNotSet(() => { + acceptFirstDataChunk(); + }, 0); + + this._socketDataListener = (data: Buffer) => { acceptFirstDataChunk(); acceptChunk(data); - }); + }; + _socket.on('data', this._socketDataListener); - _socket.on('end', () => { + this._socketEndListener = () => { acceptFirstDataChunk(); - }); + }; + _socket.on('end', this._socketEndListener); + + this._socketCloseListener = () => { + this._onClose.fire(); + }; + _socket.once('close', this._socketCloseListener); + } + + public dispose(): void { + this._isDisposed = true; + this._firstChunkTimer.dispose(); + this._socket.removeListener('data', this._socketDataListener); + this._socket.removeListener('end', this._socketEndListener); + this._socket.removeListener('close', this._socketCloseListener); + } + + public end(): void { + this._socket.end(); + } + + public getBuffer(): Buffer { + return Buffer.concat(this._chunks); } public send(message: any): void { @@ -123,10 +171,10 @@ export class Protocol implements IMessagePassingProtocol { // ensure string if (typeof message !== 'string') { message = JSON.stringify(message); - header.writeInt8(1, 0); + header.writeInt8(1, 0, true); } const data = Buffer.from(message); - header.writeInt32BE(data.length, 1); + header.writeInt32BE(data.length, 1, true); this._writeSoon(header, data); } @@ -193,18 +241,19 @@ export class Server extends IPCServer { export class Client extends IPCClient { - private _onClose = new Emitter(); - get onClose(): Event { return this._onClose.event; } + public static fromSocket(socket: Socket, id: string): Client { + return new Client(new Protocol(socket), id); + } - constructor(private socket: Socket, id: string) { - super(new Protocol(socket), id); - socket.once('close', () => this._onClose.fire()); + get onClose(): Event { return this.protocol.onClose; } + + constructor(private protocol: Protocol, id: string) { + super(protocol, id); } dispose(): void { super.dispose(); - this.socket.end(); - this.socket = null; + this.protocol.end(); } } @@ -229,7 +278,7 @@ export function connect(hook: any, clientId: string): TPromise { return new TPromise((c, e) => { const socket = createConnection(hook, () => { socket.removeListener('error', e); - c(new Client(socket, clientId)); + c(Client.fromSocket(socket, clientId)); }); socket.once('error', e); diff --git a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts index 6b48161d026..eb6ddffba02 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts @@ -87,4 +87,39 @@ suite('IPC, Socket Protocol', () => { }); }); }); + + test('can devolve to a socket and evolve again without losing data', () => { + let resolve: (v: void) => void; + let result = new TPromise((_resolve, _reject) => { + resolve = _resolve; + }); + const sender = new Protocol(stream); + const receiver1 = new Protocol(stream); + + assert.equal(stream.listenerCount('data'), 2); + assert.equal(stream.listenerCount('end'), 2); + + receiver1.onMessage((msg) => { + assert.equal(msg.value, 1); + + let buffer = receiver1.getBuffer(); + receiver1.dispose(); + + assert.equal(stream.listenerCount('data'), 1); + assert.equal(stream.listenerCount('end'), 1); + + const receiver2 = new Protocol(stream, buffer); + receiver2.onMessage((msg) => { + assert.equal(msg.value, 2); + resolve(void 0); + }); + }); + + const msg1 = { value: 1 }; + const msg2 = { value: 2 }; + sender.send(msg1); + sender.send(msg2); + + return result; + }); }); diff --git a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts index 74828296605..d19657a3f92 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts @@ -501,7 +501,8 @@ export class QuickOpenModel implements IModel, IDataSource, IFilter, - IRunner + IRunner, + IAccessiblityProvider { private _entries: QuickOpenEntry[]; private _dataSource: IDataSource; diff --git a/src/vs/base/parts/tree/browser/tree.ts b/src/vs/base/parts/tree/browser/tree.ts index efe1f73d95a..12df5ccd031 100644 --- a/src/vs/base/parts/tree/browser/tree.ts +++ b/src/vs/base/parts/tree/browser/tree.ts @@ -109,13 +109,6 @@ export interface ITree { */ collapseAll(elements?: any[], recursive?: boolean): WinJS.Promise; - /** - * Collapses several elements. - * Collapses all elements at the greatest tree depth that has expanded elements. - * The returned promise returns a boolean for whether the elements were collapsed or not. - */ - collapseDeepestExpandedLevel(): WinJS.Promise; - /** * Toggles an element's expansion state. */ diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index 032e70e54b7..ec975e735e2 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -134,6 +134,10 @@ export class DefaultController implements _.IController { return false; // Ignore event if target is a form input field (avoids browser specific issues) } + if (dom.findParentWithClass(event.target, 'scrollbar', 'monaco-tree')) { + return false; + } + if (dom.findParentWithClass(event.target, 'monaco-action-bar', 'row')) { // TODO@Joao not very nice way of checking for the action bar (implicit knowledge) return false; // Ignore event if target is over an action bar of the row } diff --git a/src/vs/base/parts/tree/browser/treeImpl.ts b/src/vs/base/parts/tree/browser/treeImpl.ts index f3b8c1415c7..10b350737ec 100644 --- a/src/vs/base/parts/tree/browser/treeImpl.ts +++ b/src/vs/base/parts/tree/browser/treeImpl.ts @@ -184,10 +184,6 @@ export class Tree implements _.ITree { return this.model.collapseAll(elements, recursive); } - public collapseDeepestExpandedLevel(): WinJS.Promise { - return this.model.collapseDeepestExpandedLevel(); - } - public toggleExpansion(element: any, recursive: boolean = false): WinJS.Promise { return this.model.toggleExpansion(element, recursive); } diff --git a/src/vs/base/parts/tree/browser/treeModel.ts b/src/vs/base/parts/tree/browser/treeModel.ts index a9caa37d358..c2323599e0b 100644 --- a/src/vs/base/parts/tree/browser/treeModel.ts +++ b/src/vs/base/parts/tree/browser/treeModel.ts @@ -559,17 +559,6 @@ export class Item { return result; } - public getChildren(): Item[] { - var child = this.firstChild; - var results = []; - while (child) { - results.push(child); - child = child.next; - } - - return results; - } - private isAncestorOf(item: Item): boolean { while (item) { if (item.id === this.id) { @@ -1040,27 +1029,6 @@ export class TreeModel { return WinJS.Promise.join(promises); } - public collapseDeepestExpandedLevel(): WinJS.Promise { - var levelToCollapse = this.findDeepestExpandedLevel(this.input, 0); - - var items = [this.input]; - for (var i = 0; i < levelToCollapse; i++) { - items = arrays.flatten(items.map(node => node.getChildren())); - } - - var promises = items.map(child => this.collapse(child, false)); - return WinJS.Promise.join(promises); - } - - private findDeepestExpandedLevel(item: Item, currentLevel: number): number { - var expandedChildren = item.getChildren().filter(child => child.isExpanded()); - if (!expandedChildren.length) { - return currentLevel; - } - - return Math.max(...expandedChildren.map(child => this.findDeepestExpandedLevel(child, currentLevel + 1))); - } - public toggleExpansion(element: any, recursive: boolean = false): WinJS.Promise { return this.isExpanded(element) ? this.collapse(element, recursive) : this.expand(element); } diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index cf6dc5ab8ff..5d56011feac 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -220,7 +220,7 @@ export class ViewItem implements IViewItem { this.element.removeAttribute('id'); } if (this.model.hasChildren()) { - this.element.setAttribute('aria-expanded', String(!!this.model.isExpanded())); + this.element.setAttribute('aria-expanded', String(!!this._styles['expanded'])); } else { this.element.removeAttribute('aria-expanded'); } diff --git a/src/vs/base/parts/tree/test/browser/treeModel.test.ts b/src/vs/base/parts/tree/test/browser/treeModel.test.ts index b65060dec56..7a23e564e4c 100644 --- a/src/vs/base/parts/tree/test/browser/treeModel.test.ts +++ b/src/vs/base/parts/tree/test/browser/treeModel.test.ts @@ -613,23 +613,6 @@ suite('TreeModel - Expansion', () => { }); }); - test('collapseDeepestExpandedLevel', () => { - return model.setInput(SAMPLE.DEEP2).then(() => { - return model.expand(SAMPLE.DEEP2.children[0]).then(() => { - return model.expand(SAMPLE.DEEP2.children[0].children[0]).then(() => { - - assert(model.isExpanded(SAMPLE.DEEP2.children[0])); - assert(model.isExpanded(SAMPLE.DEEP2.children[0].children[0])); - - return model.collapseDeepestExpandedLevel().then(() => { - assert(model.isExpanded(SAMPLE.DEEP2.children[0])); - assert(!model.isExpanded(SAMPLE.DEEP2.children[0].children[0])); - }); - }); - }); - }); - }); - test('auto expand single child folders', () => { return model.setInput(SAMPLE.DEEP).then(() => { return model.expand(SAMPLE.DEEP.children[0]).then(() => { diff --git a/src/vs/base/test/browser/ui/list/listView.test.ts b/src/vs/base/test/browser/ui/list/listView.test.ts index 5f4a644c99a..2dc54b4253f 100644 --- a/src/vs/base/test/browser/ui/list/listView.test.ts +++ b/src/vs/base/test/browser/ui/list/listView.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ListView } from 'vs/base/browser/ui/list/listView'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { range } from 'vs/base/common/arrays'; suite('ListView', function () { @@ -14,7 +14,7 @@ suite('ListView', function () { element.style.height = '200px'; element.style.width = '200px'; - const delegate: IDelegate = { + const delegate: IVirtualDelegate = { getHeight() { return 20; }, getTemplateId() { return 'template'; } }; @@ -25,6 +25,7 @@ suite('ListView', function () { templateId: 'template', renderTemplate() { templatesCount++; }, renderElement() { }, + disposeElement() { }, disposeTemplate() { templatesCount--; } }; diff --git a/src/vs/base/test/browser/ui/tree/treeModel.test.ts b/src/vs/base/test/browser/ui/tree/treeModel.test.ts index a4901161329..f348b38a9d9 100644 --- a/src/vs/base/test/browser/ui/tree/treeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/treeModel.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { TreeModel, ITreeNode } from 'vs/base/browser/ui/tree/treeModel'; -import { ISpliceable } from 'vs/base/browser/ui/list/splice'; -import { iter } from 'vs/base/common/iterator'; +import { ISpliceable } from 'vs/base/common/sequence'; +import { Iterator } from 'vs/base/common/iterator'; function toSpliceable(arr: T[]): ISpliceable { return { @@ -33,7 +33,7 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { element: 0 }, { element: 1 }, { element: 2 } @@ -55,9 +55,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -92,9 +92,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, collapsed: true, children: iter([ + element: 0, collapsed: true, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -120,7 +120,7 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { element: 0 }, { element: 1 }, { element: 2 } @@ -145,9 +145,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -179,9 +179,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -207,9 +207,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, collapsed: true, children: iter([ + element: 0, collapsed: true, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -232,9 +232,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, children: iter([ + element: 0, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -263,9 +263,9 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { - element: 0, collapsed: true, children: iter([ + element: 0, collapsed: true, children: Iterator.iterate([ { element: 10 }, { element: 11 }, { element: 12 }, @@ -303,7 +303,7 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; const model = new TreeModel(toSpliceable(list)); - model.splice([0], 0, iter([ + model.splice([0], 0, Iterator.iterate([ { element: 1, children: [ { 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 9cab77a9881..24ef5347f56 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -41,6 +41,95 @@ suite('Async', () => { return result; }); + // Cancelling a sync cancelable promise will fire the cancelled token. + // Also, every `then` callback runs in another execution frame. + test('CancelablePromise execution order (sync)', function () { + const order = []; + + const cancellablePromise = async.createCancelablePromise(token => { + order.push('in callback'); + token.onCancellationRequested(_ => order.push('cancelled')); + return Promise.resolve(1234); + }); + + order.push('afterCreate'); + + const promise = cancellablePromise + .then(null, err => null) + .then(() => order.push('finally')); + + cancellablePromise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); + }); + + // Cancelling an async cancelable promise is just the same as a sync cancellable promise. + test('CancelablePromise execution order (async)', function () { + const order = []; + + const cancellablePromise = async.createCancelablePromise(token => { + order.push('in callback'); + token.onCancellationRequested(_ => order.push('cancelled')); + return new Promise(c => setTimeout(c(1234), 0)); + }); + + order.push('afterCreate'); + + const promise = cancellablePromise + .then(null, err => null) + .then(() => order.push('finally')); + + cancellablePromise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); + }); + + // Cancelling a sync tpromise will NOT cancel the promise, since it has resolved already. + // Every `then` callback runs sync in the same execution frame, thus `finally` executes + // before `afterCancel`. + test('TPromise execution order (sync)', function () { + const order = []; + let promise = new TPromise(resolve => { + order.push('in executor'); + resolve(1234); + }, () => order.push('cancelled')); + + order.push('afterCreate'); + + promise = promise + .then(null, err => null) + .then(() => order.push('finally')); + + promise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in executor', 'afterCreate', 'finally', 'afterCancel'])); + }); + + // Cancelling an async tpromise will cancel the promise. + // Every `then` callback runs sync on the same execution frame as the `cancel` call, + // so finally still executes before `afterCancel`. + test('TPromise execution order (async)', function () { + const order = []; + let promise = new TPromise(resolve => { + order.push('in executor'); + setTimeout(() => resolve(1234)); + }, () => order.push('cancelled')); + + order.push('afterCreate'); + + promise = promise + .then(null, err => null) + .then(() => order.push('finally')); + + promise.cancel(); + order.push('afterCancel'); + + return promise.then(() => assert.deepEqual(order, ['in executor', 'afterCreate', 'cancelled', 'finally', 'afterCancel'])); + }); + test('cancelablePromise - get inner result', async function () { let promise = async.createCancelablePromise(token => { return async.timeout(12).then(_ => 1234); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 23bbe9352ce..07fc6c90f8a 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -8,7 +8,7 @@ import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map'; import * as assert from 'assert'; import URI from 'vs/base/common/uri'; -import { IIteratorResult } from 'vs/base/common/iterator'; +import { IteratorResult } from 'vs/base/common/iterator'; suite('Map', () => { @@ -419,7 +419,7 @@ suite('Map', () => { map.set('/user/foo/flip/flop', 3); map.set('/usr/foo', 4); - let item: IIteratorResult; + let item: IteratorResult; let iter = map.findSuperstr('/user'); item = iter.next(); diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index f0e948976fa..ececd9b461c 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -5,10 +5,9 @@ 'use strict'; -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, TProgressCallback, TPromise, TValueCallback } from 'vs/base/common/winjs.base'; +import { TPromise, TValueCallback } from 'vs/base/common/winjs.base'; export class DeferredTPromise extends TPromise { @@ -17,11 +16,11 @@ export class DeferredTPromise extends TPromise { private completeCallback: TValueCallback; private errorCallback: (err: any) => void; - constructor() { + constructor(oncancel?: any) { let captured: any; super((c, e) => { captured = { c, e }; - }, () => this.oncancel()); + }, oncancel ? oncancel : () => this.oncancel); this.canceled = false; this.completeCallback = captured.c; this.errorCallback = captured.e; @@ -40,39 +39,6 @@ export class DeferredTPromise extends TPromise { } } -export class DeferredPPromise extends PPromise { - - private completeCallback: TValueCallback; - private errorCallback: (err: any) => void; - private progressCallback: TProgressCallback

; - - constructor(init: (complete: TValueCallback, error: (err: any) => void, progress: TProgressCallback

) => void = (c, e, p) => { }, oncancel?: any) { - let captured: any; - super((c, e, p) => { - captured = { c, e, p }; - }, oncancel ? oncancel : () => this.oncancel); - this.completeCallback = captured.c; - this.errorCallback = captured.e; - this.progressCallback = captured.p; - } - - private oncancel(): void { - this.errorCallback(errors.canceled()); - } - - public complete(c: C) { - this.completeCallback(c); - } - - public progress(p: P) { - this.progressCallback(p); - } - - public error(e: any) { - this.errorCallback(e); - } -} - export function toResource(this: any, path: string) { return URI.file(paths.join('C:\\', Buffer.from(this.test.fullTitle()).toString('base64'), path)); } @@ -88,3 +54,7 @@ export function testRepeat(n: number, description: string, callback: (this: any, test(`${description} (iteration ${i})`, callback); } } + +export function testRepeatOnly(n: number, description: string, callback: (this: any, done: MochaDone) => any): void { + suite.only('repeat', () => testRepeat(n, description, callback)); +} 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/base/test/node/glob.test.ts b/src/vs/base/test/node/glob.test.ts index 5d2d82599e2..564399dc659 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/node/glob.test.ts @@ -430,6 +430,7 @@ suite('Glob', () => { test('expression support (single)', function () { let siblings = ['test.html', 'test.txt', 'test.ts', 'test.js']; + let hasSibling = name => siblings.indexOf(name) !== -1; // { "**/*.js": { "when": "$(basename).ts" } } let expression: glob.IExpression = { @@ -438,9 +439,9 @@ suite('Glob', () => { } }; - assert.strictEqual('**/*.js', glob.match(expression, 'test.js', () => siblings)); - assert.strictEqual(glob.match(expression, 'test.js', () => []), null); - assert.strictEqual(glob.match(expression, 'test.js', () => ['te.ts']), null); + assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); + assert.strictEqual(glob.match(expression, 'test.js', () => false), null); + assert.strictEqual(glob.match(expression, 'test.js', name => name === 'te.ts'), null); assert.strictEqual(glob.match(expression, 'test.js'), null); expression = { @@ -449,22 +450,23 @@ suite('Glob', () => { } }; - assert.strictEqual(glob.match(expression, 'test.js', () => siblings), null); + assert.strictEqual(glob.match(expression, 'test.js', hasSibling), null); expression = { '**/*.js': { } }; - assert.strictEqual('**/*.js', glob.match(expression, 'test.js', () => siblings)); + assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); expression = {}; - assert.strictEqual(glob.match(expression, 'test.js', () => siblings), null); + assert.strictEqual(glob.match(expression, 'test.js', hasSibling), null); }); test('expression support (multiple)', function () { let siblings = ['test.html', 'test.txt', 'test.ts', 'test.js']; + let hasSibling = name => siblings.indexOf(name) !== -1; // { "**/*.js": { "when": "$(basename).ts" } } let expression: glob.IExpression = { @@ -474,11 +476,11 @@ suite('Glob', () => { '**/*.bananas': { bananas: true } }; - assert.strictEqual('**/*.js', glob.match(expression, 'test.js', () => siblings)); - assert.strictEqual('**/*.as', glob.match(expression, 'test.as', () => siblings)); - assert.strictEqual('**/*.bananas', glob.match(expression, 'test.bananas', () => siblings)); + assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); + assert.strictEqual('**/*.as', glob.match(expression, 'test.as', hasSibling)); + assert.strictEqual('**/*.bananas', glob.match(expression, 'test.bananas', hasSibling)); assert.strictEqual('**/*.bananas', glob.match(expression, 'test.bananas')); - assert.strictEqual(glob.match(expression, 'test.foo', () => siblings), null); + assert.strictEqual(glob.match(expression, 'test.foo', hasSibling), null); }); test('brackets', function () { @@ -713,15 +715,16 @@ suite('Glob', () => { '**/*.js': { when: '$(basename).ts' } }; - let sibilings = () => ['foo.ts', 'foo.js', 'foo', 'bar']; + let siblings = ['foo.ts', 'foo.js', 'foo', 'bar']; + let hasSibling = name => siblings.indexOf(name) !== -1; - assert.strictEqual(glob.match(expr, 'bar', sibilings), '**/bar'); - assert.strictEqual(glob.match(expr, 'foo', sibilings), null); - assert.strictEqual(glob.match(expr, 'foo/bar', sibilings), '**/bar'); - assert.strictEqual(glob.match(expr, 'foo\\bar', sibilings), '**/bar'); - assert.strictEqual(glob.match(expr, 'foo/foo', sibilings), null); - assert.strictEqual(glob.match(expr, 'foo.js', sibilings), '**/*.js'); - assert.strictEqual(glob.match(expr, 'bar.js', sibilings), null); + assert.strictEqual(glob.match(expr, 'bar', hasSibling), '**/bar'); + assert.strictEqual(glob.match(expr, 'foo', hasSibling), null); + assert.strictEqual(glob.match(expr, 'foo/bar', hasSibling), '**/bar'); + assert.strictEqual(glob.match(expr, 'foo\\bar', hasSibling), '**/bar'); + assert.strictEqual(glob.match(expr, 'foo/foo', hasSibling), null); + assert.strictEqual(glob.match(expr, 'foo.js', hasSibling), '**/*.js'); + assert.strictEqual(glob.match(expr, 'bar.js', hasSibling), null); }); test('expression with multipe basename globs', function () { @@ -766,10 +769,11 @@ suite('Glob', () => { assert.strictEqual(glob.parse('{**/baz,**/foo}')('baz/foo', 'foo'), true); let expr = { '**/*.js': { when: '$(basename).ts' } }; - let sibilings = () => ['foo.ts', 'foo.js']; + let siblings = ['foo.ts', 'foo.js']; + let hasSibling = name => siblings.indexOf(name) !== -1; - assert.strictEqual(glob.parse(expr)('bar/baz.js', 'baz.js', sibilings), null); - assert.strictEqual(glob.parse(expr)('bar/foo.js', 'foo.js', sibilings), '**/*.js'); + assert.strictEqual(glob.parse(expr)('bar/baz.js', 'baz.js', hasSibling), null); + assert.strictEqual(glob.parse(expr)('bar/foo.js', 'foo.js', hasSibling), '**/*.js'); }); test('expression/pattern basename terms', function () { @@ -809,7 +813,8 @@ suite('Glob', () => { ['bar/nope', null] ]); - const siblingsFn = () => ['baz', 'baz.zip', 'nope']; + const siblings = ['baz', 'baz.zip', 'nope']; + const hasSibling = name => siblings.indexOf(name) !== -1; testOptimizationForBasenames({ '**/foo/**': { when: '$(basename).zip' }, '**/bar/**': true @@ -820,12 +825,12 @@ suite('Glob', () => { ['foo/bar', '**/bar/**'], ], [ null, - siblingsFn, - siblingsFn + hasSibling, + hasSibling ]); }); - function testOptimizationForBasenames(pattern: string | glob.IExpression, basenameTerms: string[], matches: [string, string | boolean][], siblingsFns: (() => string[])[] = []) { + function testOptimizationForBasenames(pattern: string | glob.IExpression, basenameTerms: string[], matches: [string, string | boolean][], siblingsFns: ((name: string) => boolean)[] = []) { const parsed = glob.parse(pattern, { trimForExclusions: true }); assert.deepStrictEqual(glob.getBasenameTerms(parsed), basenameTerms); matches.forEach(([text, result], i) => { @@ -914,7 +919,8 @@ suite('Glob', () => { [nativeSep('/foo/bar/nope'), null] ]); - const siblingsFn = () => ['baz', 'baz.zip', 'nope']; + const siblings = ['baz', 'baz.zip', 'nope']; + let hasSibling = name => siblings.indexOf(name) !== -1; testOptimizationForPaths({ '**/foo/123/**': { when: '$(basename).zip' }, '**/bar/123/**': true @@ -925,12 +931,12 @@ suite('Glob', () => { [nativeSep('foo/bar/123'), '**/bar/123/**'], ], [ null, - siblingsFn, - siblingsFn + hasSibling, + hasSibling ]); }); - function testOptimizationForPaths(pattern: string | glob.IExpression, pathTerms: string[], matches: [string, string | boolean][], siblingsFns: (() => string[])[] = []) { + function testOptimizationForPaths(pattern: string | glob.IExpression, pathTerms: string[], matches: [string, string | boolean][], siblingsFns: ((name: string) => boolean)[] = []) { const parsed = glob.parse(pattern, { trimForExclusions: true }); assert.deepStrictEqual(glob.getPathTerms(parsed), pathTerms); matches.forEach(([text, result], i) => { diff --git a/src/vs/base/test/node/uri.test.data.txt b/src/vs/base/test/node/uri.test.data.txt index 7c5ca7f53c3..bb0b5b62957 100644 --- a/src/vs/base/test/node/uri.test.data.txt +++ b/src/vs/base/test/node/uri.test.data.txt @@ -1052,7 +1052,7 @@ /users/foo/src/vs/workbench/parts/extensions/browser/extensionsList.ts /users/foo/src/vs/workbench/parts/extensions/browser/extensionsActions.ts /users/foo/src/vs/workbench/parts/extensions/browser/media -/users/foo/src/vs/workbench/parts/extensions/browser/media/language-icon.png +/users/foo/src/vs/workbench/parts/extensions/browser/media/language-icon.svg /users/foo/src/vs/workbench/parts/extensions/browser/media/theme-icon.png /users/foo/src/vs/workbench/parts/extensions/browser/media/clear.svg /users/foo/src/vs/workbench/parts/extensions/browser/media/HalfStarLight.svg diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 65980535322..a642157cdca 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -23,7 +23,7 @@ import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IWindowConfiguration, IWindowsService } from 'vs/platform/windows/common/windows'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; @@ -86,7 +86,7 @@ export class IssueReporter extends Disposable { vscodeVersion: `${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`, os: `${os.type()} ${os.arch()} ${os.release()}` }, - extensionsDisabled: this.environmentService.disableExtensions, + extensionsDisabled: !!this.environmentService.disableExtensions, }); this.previewButton = new Button(document.getElementById('issue-reporter')); @@ -287,9 +287,9 @@ export class IssueReporter extends Disposable { .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${configuration.windowId}`)); const instantiationService = new InstantiationService(serviceCollection, true); - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); - const appender = new TelemetryAppenderClient(channel); + const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)); const commonProperties = resolveCommonProperties(product.commit, pkg.version, configuration.machineId, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 97f712452c9..547d1bd99d6 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -25,7 +25,7 @@ import { ConfigurationService } from 'vs/platform/configuration/node/configurati import { IRequestService } from 'vs/platform/request/node/request'; import { RequestService } from 'vs/platform/request/electron-browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { combinedAppender, NullTelemetryService, ITelemetryAppender, NullAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; @@ -66,7 +66,8 @@ 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', { routeCall: () => 'main', routeEvent: () => 'main' })); + const mainRoute = () => TPromise.as('main'); + const logLevelClient = new LogLevelSetterChannelClient(server.getChannel('loglevel', { routeCall: mainRoute, routeEvent: mainRoute })); const logService = new FollowerLogService(logLevelClient, createSpdLogService('sharedprocess', initData.logLevel, environmentService.logsPath)); disposables.push(logService); @@ -77,46 +78,33 @@ 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', { routeCall: () => 'main', routeEvent: () => 'main' }); + const windowsChannel = server.getChannel('windows', { routeCall: mainRoute, routeEvent: mainRoute }); const windowsService = new WindowsChannelClient(windowsChannel); services.set(IWindowsService, windowsService); const activeWindowManager = new ActiveWindowManager(windowsService); - const dialogChannel = server.getChannel('dialog', { - 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; - } - }); + const route = () => activeWindowManager.getActiveClientId(); + const dialogChannel = server.getChannel('dialog', { routeCall: route, routeEvent: route }); services.set(IDialogService, new DialogChannelClient(dialogChannel)); const instantiationService = new InstantiationService(services); instantiationService.invokeFunction(accessor => { - const appenders: AppInsightsAppender[] = []; - - if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); - } - - // It is important to dispose the AI adapter properly because - // only then they flush remaining data. - disposables.push(...appenders); - - const appender = combinedAppender(...appenders); - server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appender)); - const services = new ServiceCollection(); const environmentService = accessor.get(IEnvironmentService); const { appRoot, extensionsPath, extensionDevelopmentPath, isBuilt, installSourcePath } = environmentService; + const telemetryLogService = new FollowerLogService(logLevelClient, createSpdLogService('telemetry', initData.logLevel, environmentService.logsPath)); - if (isBuilt && !extensionDevelopmentPath && !environmentService.args['disable-telemetry'] && product.enableTelemetry) { + let appInsightsAppender: ITelemetryAppender = NullAppender; + if (product.aiConfig && product.aiConfig.asimovKey && isBuilt) { + appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey, telemetryLogService); + disposables.push(appInsightsAppender); // Ensure the AI appender is disposed so that it flushes remaining data + } + server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appInsightsAppender)); + + if (!extensionDevelopmentPath && !environmentService.args['disable-telemetry'] && product.enableTelemetry) { const config: ITelemetryServiceConfig = { - appender, + appender: combinedAppender(appInsightsAppender, new LogAppender(logService)), commonProperties: resolveCommonProperties(product.commit, pkg.version, configuration.machineId, installSourcePath), piiPaths: [appRoot, extensionsPath] }; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f2a7d0cae04..a16543734c3 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -30,7 +30,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IURLService } from 'vs/platform/url/common/url'; import { URLHandlerChannelClient, URLServiceChannel } from 'vs/platform/url/common/urlIpc'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; @@ -154,14 +154,14 @@ export class CodeApplication { }); }); - let macOpenFiles: string[] = []; + let macOpenFileURIs: URI[] = []; let runningTimeout: number = null; app.on('open-file', (event: Event, path: string) => { this.logService.trace('App#open-file: ', path); event.preventDefault(); // Keep in array because more might come! - macOpenFiles.push(path); + macOpenFileURIs.push(URI.file(path)); // Clear previous handler if any if (runningTimeout !== null) { @@ -175,10 +175,10 @@ export class CodeApplication { this.windowsMainService.open({ context: OpenContext.DOCK /* can also be opening from finder while app is running */, cli: this.environmentService.args, - pathsToOpen: macOpenFiles, + urisToOpen: macOpenFileURIs, preferNewWindow: true /* dropping on the dock or opening from finder prefers to open in a new window */ }); - macOpenFiles = []; + macOpenFileURIs = []; runningTimeout = null; } }, 100); @@ -362,9 +362,9 @@ export class CodeApplication { services.set(IMenubarService, new SyncDescriptor(MenubarService)); // Telemtry - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(this.sharedProcessClient.then(c => c.getChannel('telemetryAppender'))); - const appender = new TelemetryAppenderClient(channel); + const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)); const commonProperties = resolveCommonProperties(product.commit, pkg.version, machineId, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; @@ -426,7 +426,7 @@ export class CodeApplication { // Create a URL handler which forwards to the last active window const activeWindowManager = new ActiveWindowManager(windowsService); - const route = () => activeWindowManager.activeClientId; + const route = () => activeWindowManager.getActiveClientId(); const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { routeCall: route, routeEvent: route }); const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); @@ -462,10 +462,10 @@ export class CodeApplication { // Open our first window const macOpenFiles = (global).macOpenFiles as string[]; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; - if (args['new-window'] && args._.length === 0) { + if (args['new-window'] && args._.length === 0 && (args['folder-uri'] || []).length === 0) { this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, initialStartup: true }); // new window if "-n" was used without paths - } else if (macOpenFiles && macOpenFiles.length && (!args._ || !args._.length)) { - this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, pathsToOpen: macOpenFiles, initialStartup: true }); // mac: open-file event received on startup + } else if (macOpenFiles && macOpenFiles.length && (!args._ || !args._.length || !args['folder-uri'] || !args['folder-uri'].length)) { + this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => URI.file(file)), initialStartup: true }); // mac: open-file event received on startup } else { this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!args._.length && args['unity-launch']), diffMode: args.diff, initialStartup: true }); // default: read paths from cli } diff --git a/src/vs/code/electron-main/diagnostics.ts b/src/vs/code/electron-main/diagnostics.ts index 3db04d06bdc..3bb9e182899 100644 --- a/src/vs/code/electron-main/diagnostics.ts +++ b/src/vs/code/electron-main/diagnostics.ts @@ -16,6 +16,7 @@ import { repeat, pad } from 'vs/base/common/strings'; import { isWindows } from 'vs/base/common/platform'; import { app } from 'electron'; import { basename } from 'path'; +import URI from 'vs/base/common/uri'; export interface VersionInfo { vscodeVersion: string; @@ -50,29 +51,35 @@ export function getPerformanceInfo(info: IMainProcessInfo): Promise window.folders && window.folders.length > 0)) { + if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { info.windows.forEach(window => { - if (window.folders.length === 0) { + if (window.folderURIs.length === 0) { return; } workspaceInfoMessages.push(`| Window (${window.title})`); - window.folders.forEach(folder => { - workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { + window.folderURIs.forEach(uriComponents => { + const folderUri = URI.revive(uriComponents); + if (folderUri.scheme === 'file') { + const folder = folderUri.fsPath; + workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { - let countMessage = `${stats.fileCount} files`; - if (stats.maxFilesReached) { - countMessage = `more than ${countMessage}`; - } - workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`); - workspaceInfoMessages.push(formatWorkspaceStats(stats)); + let countMessage = `${stats.fileCount} files`; + if (stats.maxFilesReached) { + countMessage = `more than ${countMessage}`; + } + workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`); + workspaceInfoMessages.push(formatWorkspaceStats(stats)); - const launchConfigs = await collectLaunchConfigs(folder); - if (launchConfigs.length > 0) { - workspaceInfoMessages.push(formatLaunchConfigs(launchConfigs)); - } - })); + const launchConfigs = await collectLaunchConfigs(folder); + if (launchConfigs.length > 0) { + workspaceInfoMessages.push(formatLaunchConfigs(launchConfigs)); + } + })); + } else { + workspaceInfoMessages.push(`| Folder (${folderUri.toString()}): RPerformance stats not available.`); + } }); }); } @@ -129,33 +136,39 @@ export function printDiagnostics(info: IMainProcessInfo): Promise { // Workspace Stats const workspaceStatPromises = []; - if (info.windows.some(window => window.folders && window.folders.length > 0)) { + if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { console.log(''); console.log('Workspace Stats: '); info.windows.forEach(window => { - if (window.folders.length === 0) { + if (window.folderURIs.length === 0) { return; } console.log(`| Window (${window.title})`); - window.folders.forEach(folder => { - workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { - let countMessage = `${stats.fileCount} files`; - if (stats.maxFilesReached) { - countMessage = `more than ${countMessage}`; - } - console.log(`| Folder (${basename(folder)}): ${countMessage}`); - console.log(formatWorkspaceStats(stats)); - - await collectLaunchConfigs(folder).then(launchConfigs => { - if (launchConfigs.length > 0) { - console.log(formatLaunchConfigs(launchConfigs)); + window.folderURIs.forEach(uriComponents => { + const folderUri = URI.revive(uriComponents); + if (folderUri.scheme === 'file') { + const folder = folderUri.fsPath; + workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { + let countMessage = `${stats.fileCount} files`; + if (stats.maxFilesReached) { + countMessage = `more than ${countMessage}`; } - }); - }).catch(error => { - console.log(`| Error: Unable to collect workpsace stats for folder ${folder} (${error.toString()})`); - })); + console.log(`| Folder (${basename(folder)}): ${countMessage}`); + console.log(formatWorkspaceStats(stats)); + + await collectLaunchConfigs(folder).then(launchConfigs => { + if (launchConfigs.length > 0) { + console.log(formatLaunchConfigs(launchConfigs)); + } + }); + }).catch(error => { + console.log(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`); + })); + } else { + console.log(`| Folder (${folderUri.toString()}): Workspace stats not available.`); + } }); }); } diff --git a/src/vs/code/electron-main/launch.ts b/src/vs/code/electron-main/launch.ts index d1781a1a89b..2cad34a8d84 100644 --- a/src/vs/code/electron-main/launch.ts +++ b/src/vs/code/electron-main/launch.ts @@ -16,9 +16,8 @@ import { OpenContext, IWindowSettings } from 'vs/platform/windows/common/windows import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; -import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import URI from 'vs/base/common/uri'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { BrowserWindow } from 'electron'; import { Event } from 'vs/base/common/event'; @@ -33,7 +32,7 @@ export interface IStartArguments { export interface IWindowInfo { pid: number; title: string; - folders: string[]; + folderURIs: UriComponents[]; } export interface IMainProcessInfo { @@ -179,7 +178,7 @@ export class LaunchService implements ILaunchService { } // Start without file/folder arguments - else if (args._.length === 0) { + else if (args._.length === 0 && (args['folder-uri'] || []).length === 0) { let openNewWindow = false; // Force new window @@ -275,27 +274,25 @@ export class LaunchService implements ILaunchService { } private codeWindowToInfo(window: ICodeWindow): IWindowInfo { - const folders: string[] = []; + const folderURIs: URI[] = []; - if (window.openedFolderPath) { - folders.push(window.openedFolderPath); + if (window.openedFolderUri) { + folderURIs.push(window.openedFolderUri); } else if (window.openedWorkspace) { const rootFolders = this.workspacesMainService.resolveWorkspaceSync(window.openedWorkspace.configPath).folders; rootFolders.forEach(root => { - if (root.uri.scheme === Schemas.file) { // todo@remote signal remote folders? - folders.push(root.uri.fsPath); - } + folderURIs.push(root.uri); }); } - return this.browserWindowToInfo(window.win, folders); + return this.browserWindowToInfo(window.win, folderURIs); } - private browserWindowToInfo(win: BrowserWindow, folders: string[] = []): IWindowInfo { + private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = []): IWindowInfo { return { pid: win.webContents.getOSProcessId(), title: win.getTitle(), - folders + folderURIs } as IWindowInfo; } } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 4349d32b4f8..134f4379fb3 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -35,7 +35,7 @@ import { IRequestService } from 'vs/platform/request/node/request'; import { RequestService } from 'vs/platform/request/electron-main/requestService'; import { IURLService } from 'vs/platform/url/common/url'; import { URLService } from 'vs/platform/url/common/urlService'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import { CodeApplication } from 'vs/code/electron-main/app'; import { HistoryMainService } from 'vs/platform/history/electron-main/historyMainService'; import { IHistoryMainService } from 'vs/platform/history/common/history'; diff --git a/src/vs/code/electron-main/menubar.ts b/src/vs/code/electron-main/menubar.ts index 5870ea0ad31..84c15cd5cf0 100644 --- a/src/vs/code/electron-main/menubar.ts +++ b/src/vs/code/electron-main/menubar.ts @@ -20,8 +20,9 @@ import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel, getPathLabel } import { KeybindingsResolver } from 'vs/code/electron-main/keyboard'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IMenubarData, IMenubarMenuItemAction, IMenubarMenuItemSeparator } from 'vs/platform/menubar/common/menubar'; +import URI from 'vs/base/common/uri'; // interface IExtensionViewlet { // id: string; @@ -513,13 +514,16 @@ export class Menubar { private createOpenRecentMenuItem(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): Electron.MenuItem { let label: string; - let path: string; - if (isSingleFolderWorkspaceIdentifier(workspace) || typeof workspace === 'string') { - label = unmnemonicLabel(getPathLabel(workspace, this.environmentService, null)); - path = workspace; - } else { + let uri: URI; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + label = unmnemonicLabel(getWorkspaceLabel(workspace, this.environmentService, { verbose: true })); + uri = workspace; + } else if (isWorkspaceIdentifier(workspace)) { label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); - path = workspace.configPath; + uri = URI.file(workspace.configPath); + } else { + label = unmnemonicLabel(getPathLabel(workspace, this.environmentService, null)); + uri = URI.file(workspace); } return new MenuItem(this.likeAction(commandId, { @@ -529,12 +533,13 @@ export class Menubar { const success = this.windowsMainService.open({ context: OpenContext.MENU, cli: this.environmentService.args, - pathsToOpen: [path], forceNewWindow: openInNewWindow, + urisToOpen: [uri], + forceNewWindow: openInNewWindow, forceOpenWorkspaceAsFile: isFile }).length > 0; if (!success) { - this.historyMainService.removeFromRecentlyOpened([isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath]); + this.historyMainService.removeFromRecentlyOpened([workspace]); } } }, false)); diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index c1a4ddf0b1a..54cf3480930 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -22,7 +22,8 @@ import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel, getPathLabel } import { KeybindingsResolver } from 'vs/code/electron-main/keyboard'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import URI from 'vs/base/common/uri'; interface IMenuItemClickHandler { inDevTools: (contents: Electron.WebContents) => void; @@ -194,7 +195,7 @@ export class CodeMenu { private updateWorkspaceMenuItems(): void { const window = this.windowsMainService.getLastActiveWindow(); const isInWorkspaceContext = window && !!window.openedWorkspace; - const isInFolderContext = window && !!window.openedFolderPath; + const isInFolderContext = window && !!window.openedFolderUri; this.closeWorkspace.visible = isInWorkspaceContext; this.closeFolder.visible = !isInWorkspaceContext; @@ -241,7 +242,7 @@ export class CodeMenu { // Terminal const terminalMenu = new Menu(); - const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "Ter&&minal")), submenu: terminalMenu }); this.setTerminalMenu(terminalMenu); // Debug @@ -435,7 +436,7 @@ export class CodeMenu { } private getPreferencesMenu(): Electron.MenuItem { - const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openSettings'); + const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openSettings2'); const kebindingSettings = this.createMenuItem(nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts"), 'workbench.action.openGlobalKeybindings'); const keymapExtensions = this.createMenuItem(nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions"), 'workbench.extensions.action.showRecommendedKeymapExtensions'); const snippetsSettings = this.createMenuItem(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), 'workbench.action.openSnippets'); @@ -489,13 +490,16 @@ export class CodeMenu { private createOpenRecentMenuItem(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): Electron.MenuItem { let label: string; - let path: string; - if (isSingleFolderWorkspaceIdentifier(workspace) || typeof workspace === 'string') { - label = unmnemonicLabel(getPathLabel(workspace, this.environmentService)); - path = workspace; - } else { + let uri: URI; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + label = unmnemonicLabel(getWorkspaceLabel(workspace, this.environmentService, { verbose: true })); + uri = workspace; + } else if (isWorkspaceIdentifier(workspace)) { label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); - path = workspace.configPath; + uri = URI.file(workspace.configPath); + } else { + label = unmnemonicLabel(getPathLabel(workspace, this.environmentService, null)); + uri = URI.file(workspace); } return new MenuItem(this.likeAction(commandId, { @@ -505,12 +509,12 @@ export class CodeMenu { const success = this.windowsMainService.open({ context: OpenContext.MENU, cli: this.environmentService.args, - pathsToOpen: [path], forceNewWindow: openInNewWindow, + urisToOpen: [uri], forceNewWindow: openInNewWindow, forceOpenWorkspaceAsFile: isFile }).length > 0; if (!success) { - this.historyMainService.removeFromRecentlyOpened([isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath]); + this.historyMainService.removeFromRecentlyOpened([workspace]); } } }, false)); @@ -662,52 +666,15 @@ export class CodeMenu { 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'); + // Appearance + + const appearanceMenu = new Menu(); + 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 })); const toggleZenMode = this.createMenuItem(nls.localize('miToggleZenMode', "Toggle Zen Mode"), 'workbench.action.toggleZenMode'); const toggleCenteredLayout = this.createMenuItem(nls.localize('miToggleCenteredLayout', "Toggle Centered Layout"), 'workbench.action.toggleCenteredLayout'); const toggleMenuBar = this.createMenuItem(nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar"), 'workbench.action.toggleMenuBar'); - // Editor Layout - - const editorLayoutMenu = new Menu(); - - const splitEditorUp = this.createMenuItem(nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up"), 'workbench.action.splitEditorUp'); - const splitEditorDown = this.createMenuItem(nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down"), 'workbench.action.splitEditorDown'); - const splitEditorLeft = this.createMenuItem(nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left"), 'workbench.action.splitEditorLeft'); - const splitEditorRight = this.createMenuItem(nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right"), 'workbench.action.splitEditorRight'); - - const singleColumnEditorLayout = this.createMenuItem(nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single"), 'workbench.action.editorLayoutSingle'); - const twoColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns"), 'workbench.action.editorLayoutTwoColumns'); - const threeColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns"), 'workbench.action.editorLayoutThreeColumns'); - 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 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'); - - [ - splitEditorUp, - splitEditorDown, - splitEditorLeft, - splitEditorRight, - __separator__(), - singleColumnEditorLayout, - twoColumnsEditorLayout, - threeColumnsEditorLayout, - twoRowsEditorLayout, - threeRowsEditorLayout, - twoByTwoGridEditorLayout, - twoColumnsRightEditorLayout, - twoColumnsBottomEditorLayout, - __separator__(), - toggleEditorLayout - ].forEach(item => editorLayoutMenu.append(item)); - - const editorLayout = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout")), submenu: editorLayoutMenu }); - - // Workbench Layout const toggleSidebar = this.createMenuItem(nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar"), 'workbench.action.toggleSidebarVisibility'); let moveSideBarLabel: string; @@ -736,21 +703,82 @@ export class CodeMenu { } const toggleActivtyBar = this.createMenuItem(activityBarLabel, 'workbench.action.toggleActivityBarVisibility'); - // Editor - const toggleWordWrap = this.createMenuItem(nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), 'editor.action.toggleWordWrap'); - const toggleMinimap = this.createMenuItem(nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap"), 'editor.action.toggleMinimap'); - const toggleRenderWhitespace = this.createMenuItem(nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"), 'editor.action.toggleRenderWhitespace'); - const toggleRenderControlCharacters = this.createMenuItem(nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters"), 'editor.action.toggleRenderControlCharacter'); - - // Zoom const zoomIn = this.createMenuItem(nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"), 'workbench.action.zoomIn'); const zoomOut = this.createMenuItem(nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "Zoom O&&ut"), 'workbench.action.zoomOut'); const resetZoom = this.createMenuItem(nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"), 'workbench.action.zoomReset'); + arrays.coalesce([ + fullscreen, + toggleZenMode, + toggleCenteredLayout, + isWindows || isLinux ? toggleMenuBar : void 0, + __separator__(), + moveSidebar, + toggleSidebar, + togglePanel, + toggleStatusbar, + toggleActivtyBar, + __separator__(), + zoomIn, + zoomOut, + resetZoom + ]).forEach(item => appearanceMenu.append(item)); + + const appearance = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance")), submenu: appearanceMenu }); + + // Editor Layout + + const editorLayoutMenu = new Menu(); + + const splitEditorUp = this.createMenuItem(nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up"), 'workbench.action.splitEditorUp'); + const splitEditorDown = this.createMenuItem(nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down"), 'workbench.action.splitEditorDown'); + const splitEditorLeft = this.createMenuItem(nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left"), 'workbench.action.splitEditorLeft'); + const splitEditorRight = this.createMenuItem(nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right"), 'workbench.action.splitEditorRight'); + + const singleColumnEditorLayout = this.createMenuItem(nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single"), 'workbench.action.editorLayoutSingle'); + const twoColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns"), 'workbench.action.editorLayoutTwoColumns'); + const threeColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns"), 'workbench.action.editorLayoutThreeColumns'); + 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 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'); + + [ + splitEditorUp, + splitEditorDown, + splitEditorLeft, + splitEditorRight, + __separator__(), + singleColumnEditorLayout, + twoColumnsEditorLayout, + threeColumnsEditorLayout, + twoRowsEditorLayout, + threeRowsEditorLayout, + twoByTwoGridEditorLayout, + twoRowsRightEditorLayout, + twoColumnsBottomEditorLayout, + __separator__(), + toggleEditorLayout + ].forEach(item => editorLayoutMenu.append(item)); + + const editorLayout = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout")), submenu: editorLayoutMenu }); + + const toggleWordWrap = this.createMenuItem(nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), 'editor.action.toggleWordWrap'); + const toggleMinimap = this.createMenuItem(nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap"), 'editor.action.toggleMinimap'); + const toggleRenderWhitespace = this.createMenuItem(nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"), 'editor.action.toggleRenderWhitespace'); + const toggleRenderControlCharacters = this.createMenuItem(nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters"), 'editor.action.toggleRenderControlCharacter'); + const toggleBreadcrumbs = this.createMenuItem(nls.localize({ key: 'miToggleBreadcrumbs', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breadcrumbs"), 'breadcrumbs.toggle'); + arrays.coalesce([ commands, openView, __separator__(), + appearance, + editorLayout, + __separator__(), explorer, search, scm, @@ -762,27 +790,11 @@ export class CodeMenu { debugConsole, terminal, __separator__(), - fullscreen, - toggleZenMode, - toggleCenteredLayout, - isWindows || isLinux ? toggleMenuBar : void 0, - __separator__(), - editorLayout, - __separator__(), - moveSidebar, - toggleSidebar, - togglePanel, - toggleStatusbar, - toggleActivtyBar, - __separator__(), toggleWordWrap, toggleMinimap, toggleRenderWhitespace, toggleRenderControlCharacters, - __separator__(), - zoomIn, - zoomOut, - resetZoom + toggleBreadcrumbs ]).forEach(item => viewMenu.append(item)); } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 5c70b90de69..11099c63863 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -297,8 +297,8 @@ export class CodeWindow implements ICodeWindow { return this.currentConfig ? this.currentConfig.workspace : void 0; } - get openedFolderPath(): string { - return this.currentConfig ? this.currentConfig.folderPath : void 0; + get openedFolderUri(): URI { + return this.currentConfig ? this.currentConfig.folderUri : void 0; } setReady(): void { diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 04d9d28c4ee..4979ce9df99 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -6,7 +6,7 @@ 'use strict'; import { basename, normalize, join, dirname } from 'path'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import { localize } from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; import { assign, mixin, equals } from 'vs/base/common/objects'; @@ -20,16 +20,15 @@ import { ILifecycleService, UnloadReason, IWindowUnloadEvent } from 'vs/platform import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, ReadyState, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderPath } from 'vs/code/node/windowsFinder'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/code/node/windowsFinder'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import product from 'vs/platform/node/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { isEqual } from 'vs/base/common/paths'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IWorkspacesMainService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, WORKSPACE_FILTER, isSingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; @@ -37,6 +36,7 @@ import { normalizeNFC } from 'vs/base/common/normalization'; import URI from 'vs/base/common/uri'; import { Queue } from 'vs/base/common/async'; import { exists } from 'vs/base/node/pfs'; +import { getComparisonKey, isEqual, hasToIgnoreCase } from 'vs/base/common/resources'; enum WindowError { UNRESPONSIVE, @@ -49,11 +49,15 @@ interface INewWindowState extends ISingleWindowState { interface IWindowState { workspace?: IWorkspaceIdentifier; - folderPath?: string; + folderUri?: URI; backupPath: string; uiState: ISingleWindowState; } +interface IBackwardCompatibleWindowState extends IWindowState { + folderPath?: string; +} + interface IWindowsState { lastActiveWindow?: IWindowState; lastPluginDevelopmentHostWindow?: IWindowState; @@ -67,7 +71,7 @@ interface IOpenBrowserWindowOptions { cli?: ParsedArgs; workspace?: IWorkspaceIdentifier; - folderPath?: string; + folderUri?: URI; initialStartup?: boolean; @@ -88,7 +92,7 @@ interface IPathToOpen extends IPath { workspace?: IWorkspaceIdentifier; // the folder path for a Code instance to open - folderPath?: string; + folderUri?: URI; // the backup spath for a Code instance to use backupPath?: string; @@ -144,7 +148,7 @@ export class WindowsManager implements IWindowsMainService { @IWorkspacesMainService private workspacesMainService: IWorkspacesMainService, @IInstantiationService private instantiationService: IInstantiationService ) { - this.windowsState = this.stateService.getItem(WindowsManager.windowsStateStorageKey) || { openedWindows: [] }; + this.windowsState = this.getWindowsState(); if (!Array.isArray(this.windowsState.openedWindows)) { this.windowsState.openedWindows = []; } @@ -153,6 +157,30 @@ export class WindowsManager implements IWindowsMainService { this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, this); } + private getWindowsState(): IWindowsState { + const windowsState = this.stateService.getItem(WindowsManager.windowsStateStorageKey) || { openedWindows: [] }; + if (windowsState.lastActiveWindow) { + windowsState.lastActiveWindow = this.revive(windowsState.lastActiveWindow); + } + if (windowsState.lastPluginDevelopmentHostWindow) { + windowsState.lastPluginDevelopmentHostWindow = this.revive(windowsState.lastPluginDevelopmentHostWindow); + } + if (windowsState.openedWindows) { + windowsState.openedWindows = windowsState.openedWindows.map(windowState => this.revive(windowState)); + } + return windowsState; + } + + private revive(windowState: IWindowState): IWindowState { + if (windowState.folderUri) { + windowState.folderUri = URI.revive(windowState.folderUri); + } + if ((windowState).folderPath) { + windowState.folderUri = URI.file((windowState).folderPath); + } + return windowState; + } + ready(initialUserEnv: IProcessEnvironment): void { this.initialUserEnv = initialUserEnv; @@ -294,10 +322,10 @@ export class WindowsManager implements IWindowsMainService { } // Any non extension host window with same workspace or folder - else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderPath)) { + else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) { this.windowsState.openedWindows.forEach(o => { const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id; - const sameFolder = win.openedFolderPath && isEqual(o.folderPath, win.openedFolderPath, !isLinux /* ignorecase */); + const sameFolder = win.openedFolderUri && o.folderUri && isEqual(o.folderUri, win.openedFolderUri, hasToIgnoreCase(o.folderUri)); if (sameWorkspace || sameFolder) { o.uiState = state.uiState; @@ -317,23 +345,24 @@ export class WindowsManager implements IWindowsMainService { private toWindowState(win: ICodeWindow): IWindowState { return { workspace: win.openedWorkspace, - folderPath: win.openedFolderPath, + folderUri: win.openedFolderUri, backupPath: win.backupPath, uiState: win.serializeWindowState() }; } open(openConfig: IOpenConfiguration): ICodeWindow[] { + this.logService.trace('windowsManager#open'); openConfig = this.validateOpenConfig(openConfig); let pathsToOpen = this.getPathsToOpen(openConfig); // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. - let foldersToAdd: IPath[] = []; + let foldersToAdd: URI[] = []; if (openConfig.addMode) { - foldersToAdd = pathsToOpen.filter(path => !!path.folderPath).map(path => ({ filePath: path.folderPath })); - pathsToOpen = pathsToOpen.filter(path => !path.folderPath); + foldersToAdd = pathsToOpen.filter(path => !!path.folderUri).map(path => path.folderUri); + pathsToOpen = pathsToOpen.filter(path => !path.folderUri); } let filesToOpen = pathsToOpen.filter(path => !!path.filePath && !path.createFilePath); @@ -362,12 +391,12 @@ export class WindowsManager implements IWindowsMainService { // // These are windows to open to show either folders or files (including diffing files or creating them) // - const foldersToOpen = arrays.distinct(pathsToOpen.filter(win => win.folderPath && !win.filePath).map(win => win.folderPath), folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates + const foldersToOpen = arrays.distinct(pathsToOpen.filter(win => win.folderUri && !win.filePath).map(win => win.folderUri), folder => getComparisonKey(folder)); // prevent duplicates // // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) // - let foldersToRestore: string[] = []; + let foldersToRestore: URI[] = []; let workspacesToRestore: IWorkspaceIdentifier[] = []; let emptyToRestore: string[] = []; if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) { @@ -377,21 +406,22 @@ export class WindowsManager implements IWindowsMainService { workspacesToRestore.push(...this.workspacesMainService.getUntitledWorkspacesSync()); // collect from previous window session emptyToRestore = this.backupMainService.getEmptyWindowBackupPaths(); - emptyToRestore.push(...pathsToOpen.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => basename(w.backupPath))); // add empty windows with backupPath + emptyToRestore.push(...pathsToOpen.filter(w => !w.workspace && !w.folderUri && w.backupPath).map(w => basename(w.backupPath))); // add empty windows with backupPath emptyToRestore = arrays.distinct(emptyToRestore); // prevent duplicates } // // These are empty windows to open // - const emptyToOpen = pathsToOpen.filter(win => !win.workspace && !win.folderPath && !win.filePath && !win.backupPath).length; + const emptyToOpen = pathsToOpen.filter(win => !win.workspace && !win.folderUri && !win.filePath && !win.backupPath).length; // Open based on config const usedWindows = this.doOpen(openConfig, workspacesToOpen, workspacesToRestore, foldersToOpen, foldersToRestore, emptyToRestore, emptyToOpen, filesToOpen, filesToCreate, filesToDiff, filesToWait, foldersToAdd); // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { - let focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && (!openConfig.pathsToOpen || !openConfig.pathsToOpen.length); + + let focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !(openConfig.cli['folder-uri'] || []).length && !(openConfig.urisToOpen || []).length; let focusLastOpened = true; let focusLastWindow = true; @@ -410,9 +440,9 @@ export class WindowsManager implements IWindowsMainService { for (let i = usedWindows.length - 1; i >= 0; i--) { const usedWindow = usedWindows[i]; if ( - (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace - (usedWindow.openedFolderPath && foldersToRestore.some(folder => folder === usedWindow.openedFolderPath)) || // skip over restored folder - (usedWindow.backupPath && emptyToRestore.some(empty => empty === basename(usedWindow.backupPath))) // skip over restored empty window + (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace + (usedWindow.openedFolderUri && foldersToRestore.some(folder => isEqual(folder, usedWindow.openedFolderUri, hasToIgnoreCase(folder)))) || // skip over restored folder + (usedWindow.backupPath && emptyToRestore.some(empty => empty === basename(usedWindow.backupPath))) // skip over restored empty window ) { continue; } @@ -436,8 +466,8 @@ export class WindowsManager implements IWindowsMainService { const recentlyOpenedFiles: string[] = []; pathsToOpen.forEach(win => { - if (win.workspace || win.folderPath) { - recentlyOpenedWorkspaces.push(win.workspace || win.folderPath); + if (win.workspace || win.folderUri) { + recentlyOpenedWorkspaces.push(win.workspace || win.folderUri); } else if (win.filePath) { recentlyOpenedFiles.push(win.filePath); } @@ -472,15 +502,15 @@ export class WindowsManager implements IWindowsMainService { openConfig: IOpenConfiguration, workspacesToOpen: IWorkspaceIdentifier[], workspacesToRestore: IWorkspaceIdentifier[], - foldersToOpen: string[], - foldersToRestore: string[], + foldersToOpen: URI[], + foldersToRestore: URI[], emptyToRestore: string[], emptyToOpen: number, filesToOpen: IPath[], filesToCreate: IPath[], filesToDiff: IPath[], filesToWait: IPathsToWaitFor, - foldersToAdd: IPath[] + foldersToAdd: URI[] ) { const usedWindows: ICodeWindow[] = []; @@ -516,6 +546,8 @@ export class WindowsManager implements IWindowsMainService { // Special case: we started with --wait and we got back a folder to open. In this case // we actually prefer to not open the folder but operate purely on the file. if (typeof bestWindowOrFolder === 'string' && filesToWait) { + //TODO: #54483 Ben This should not happen + console.error(`This should not happen`, bestWindowOrFolder, WindowsManager.WINDOWS); bestWindowOrFolder = !openFilesInNewWindow ? this.getLastActiveWindow() : null; } @@ -528,8 +560,8 @@ export class WindowsManager implements IWindowsMainService { } // Window is single folder - else if (bestWindowOrFolder.openedFolderPath) { - foldersToOpen.push(bestWindowOrFolder.openedFolderPath); + else if (bestWindowOrFolder.openedFolderUri) { + foldersToOpen.push(bestWindowOrFolder.openedFolderUri); } // Window is empty @@ -548,7 +580,9 @@ export class WindowsManager implements IWindowsMainService { // We found a suitable folder to open: add it to foldersToOpen else if (typeof bestWindowOrFolder === 'string') { - foldersToOpen.push(bestWindowOrFolder); + //TODO: #54483 Ben This should not happen + // foldersToOpen.push(bestWindowOrFolder); + console.error(`This should not happen`, bestWindowOrFolder, WindowsManager.WINDOWS); } // Finally, if no window or folder is found, just open the files in an empty window @@ -613,7 +647,8 @@ export class WindowsManager implements IWindowsMainService { } // Handle folders to open (instructed and to restore) - const allFoldersToOpen = arrays.distinct([...foldersToRestore, ...foldersToOpen], folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates + const allFoldersToOpen = arrays.distinct([...foldersToRestore, ...foldersToOpen], folder => getComparisonKey(folder)); // prevent duplicates + if (allFoldersToOpen.length > 0) { // Check for existing instances @@ -635,12 +670,13 @@ export class WindowsManager implements IWindowsMainService { // Open remaining ones allFoldersToOpen.forEach(folderToOpen => { - if (windowsOnFolderPath.some(win => isEqual(win.openedFolderPath, folderToOpen, !isLinux /* ignorecase */))) { + + if (windowsOnFolderPath.some(win => isEqual(win.openedFolderUri, folderToOpen, hasToIgnoreCase(win.openedFolderUri)))) { return; // ignore folders that are already open } // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { folderPath: folderToOpen }, openFolderInNewWindow, filesToOpen, filesToCreate, filesToDiff, filesToWait)); + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { folderUri: folderToOpen }, openFolderInNewWindow, filesToOpen, filesToCreate, filesToDiff, filesToWait)); // Reset these because we handled them filesToOpen = []; @@ -705,7 +741,7 @@ export class WindowsManager implements IWindowsMainService { return window; } - private doAddFoldersToExistingWidow(window: ICodeWindow, foldersToAdd: IPath[]): ICodeWindow { + private doAddFoldersToExistingWidow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { window.focus(); // make sure window has focus window.ready().then(readyWindow => { @@ -725,7 +761,7 @@ export class WindowsManager implements IWindowsMainService { cli: openConfig.cli, initialStartup: openConfig.initialStartup, workspace: folderOrWorkspace.workspace, - folderPath: folderOrWorkspace.folderPath, + folderUri: folderOrWorkspace.folderUri, filesToOpen, filesToCreate, filesToDiff, @@ -742,7 +778,7 @@ export class WindowsManager implements IWindowsMainService { let isCommandLineOrAPICall = false; // Extract paths: from API - if (openConfig.pathsToOpen && openConfig.pathsToOpen.length > 0) { + if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { windowsToOpen = this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } @@ -753,7 +789,7 @@ export class WindowsManager implements IWindowsMainService { } // Extract paths: from CLI - else if (openConfig.cli._.length > 0) { + else if (openConfig.cli._.length > 0 || (openConfig.cli['folder-uri'] || []).length > 0) { windowsToOpen = this.doExtractPathsFromCLI(openConfig.cli); isCommandLineOrAPICall = true; } @@ -768,13 +804,13 @@ export class WindowsManager implements IWindowsMainService { // If we are in addMode, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { - const foldersToOpen = windowsToOpen.filter(path => !!path.folderPath); + const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); if (foldersToOpen.length > 1) { - const workspace = this.workspacesMainService.createWorkspaceSync(foldersToOpen.map(folder => ({ uri: URI.file(folder.folderPath) }))); + const workspace = this.workspacesMainService.createWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri }))); // Add workspace and remove folders thereby windowsToOpen.push({ workspace }); - windowsToOpen = windowsToOpen.filter(path => !path.folderPath); + windowsToOpen = windowsToOpen.filter(path => !path.folderUri); } } @@ -782,8 +818,8 @@ export class WindowsManager implements IWindowsMainService { } private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPath[] { - let pathsToOpen = openConfig.pathsToOpen.map(pathToOpen => { - const path = this.parsePath(pathToOpen, { gotoLineMode: openConfig.cli && openConfig.cli.goto, forceOpenWorkspaceAsFile: openConfig.forceOpenWorkspaceAsFile }); + let pathsToOpen = openConfig.urisToOpen.map(pathToOpen => { + const path = this.parseUri(pathToOpen, { gotoLineMode: openConfig.cli && openConfig.cli.goto, forceOpenWorkspaceAsFile: openConfig.forceOpenWorkspaceAsFile }); // Warn if the requested path to open does not exist if (!path) { @@ -792,7 +828,7 @@ export class WindowsManager implements IWindowsMainService { type: 'info', buttons: [localize('ok', "OK")], message: localize('pathNotExistTitle', "Path does not exist"), - detail: localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", pathToOpen), + detail: localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", pathToOpen.scheme === Schemas.file ? pathToOpen.fsPath : pathToOpen.path), noLink: true }; @@ -809,7 +845,20 @@ export class WindowsManager implements IWindowsMainService { } private doExtractPathsFromCLI(cli: ParsedArgs): IPath[] { - const pathsToOpen = arrays.coalesce(cli._.map(candidate => this.parsePath(candidate, { ignoreFileNotFound: true, gotoLineMode: cli.goto }))); + const pathsToOpen = []; + + // folder uris + if (cli['folder-uri'] && cli['folder-uri'].length) { + const arg = cli['folder-uri']; + const folderUris: string[] = typeof arg === 'string' ? [arg] : arg; + pathsToOpen.push(...arrays.coalesce(folderUris.map(candidate => this.parseUri(URI.parse(candidate), { ignoreFileNotFound: true, gotoLineMode: cli.goto })))); + } + + // folder or file paths + if (cli._ && cli._.length) { + pathsToOpen.push(...arrays.coalesce(cli._.map(candidate => this.parsePath(candidate, { ignoreFileNotFound: true, gotoLineMode: cli.goto })))); + } + if (pathsToOpen.length > 0) { return pathsToOpen; } @@ -842,9 +891,9 @@ export class WindowsManager implements IWindowsMainService { } // folder (if path is valid) - else if (lastActiveWindow.folderPath) { - const validatedFolder = this.parsePath(lastActiveWindow.folderPath); - if (validatedFolder && validatedFolder.folderPath) { + else if (lastActiveWindow.folderUri) { + const validatedFolder = this.parseUri(lastActiveWindow.folderUri); + if (validatedFolder && validatedFolder.folderUri) { return [validatedFolder]; } } @@ -870,16 +919,16 @@ export class WindowsManager implements IWindowsMainService { windowsToOpen.push(...workspaceCandidates.map(candidate => this.parsePath(candidate.configPath)).filter(window => window && window.workspace)); // Folders - const folderCandidates = this.windowsState.openedWindows.filter(w => !!w.folderPath).map(w => w.folderPath); - if (lastActiveWindow && lastActiveWindow.folderPath) { - folderCandidates.push(lastActiveWindow.folderPath); + const folderCandidates = this.windowsState.openedWindows.filter(w => !!w.folderUri).map(w => w.folderUri); + if (lastActiveWindow && lastActiveWindow.folderUri) { + folderCandidates.push(lastActiveWindow.folderUri); } - windowsToOpen.push(...folderCandidates.map(candidate => this.parsePath(candidate)).filter(window => window && window.folderPath)); + windowsToOpen.push(...folderCandidates.map(candidate => this.parseUri(candidate)).filter(window => window && window.folderUri)); // Windows that were Empty if (restoreWindows === 'all') { - const lastOpenedEmpty = this.windowsState.openedWindows.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => w.backupPath); - const lastActiveEmpty = lastActiveWindow && !lastActiveWindow.workspace && !lastActiveWindow.folderPath && lastActiveWindow.backupPath; + const lastOpenedEmpty = this.windowsState.openedWindows.filter(w => !w.workspace && !w.folderUri && w.backupPath).map(w => w.backupPath); + const lastActiveEmpty = lastActiveWindow && !lastActiveWindow.workspace && !lastActiveWindow.folderUri && lastActiveWindow.backupPath; if (lastActiveEmpty) { lastOpenedEmpty.push(lastActiveEmpty); } @@ -914,6 +963,20 @@ export class WindowsManager implements IWindowsMainService { return restoreWindows; } + private parseUri(anyUri: URI, options?: { ignoreFileNotFound?: boolean, gotoLineMode?: boolean, forceOpenWorkspaceAsFile?: boolean; }): IPathToOpen { + if (!anyUri) { + return null; + } + + if (anyUri.scheme === Schemas.file) { + return this.parsePath(anyUri.fsPath, options); + } + + return { + folderUri: anyUri + }; + } + private parsePath(anyPath: string, options?: { ignoreFileNotFound?: boolean, gotoLineMode?: boolean, forceOpenWorkspaceAsFile?: boolean; }): IPathToOpen { if (!anyPath) { return null; @@ -954,7 +1017,7 @@ export class WindowsManager implements IWindowsMainService { // over to us) else if (candidateStat.isDirectory()) { return { - folderPath: candidate + folderUri: URI.file(candidate) }; } } @@ -1024,25 +1087,39 @@ export class WindowsManager implements IWindowsMainService { } // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing - if (openConfig.cli._.length === 0 && !openConfig.cli.extensionTestsPath) { + if (openConfig.cli._.length === 0 && (openConfig.cli['folder-uri'] || []).length === 0 && !openConfig.cli.extensionTestsPath) { const extensionDevelopmentWindowState = this.windowsState.lastPluginDevelopmentHostWindow; - const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderPath); + const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderUri); if (workspaceToOpen) { - openConfig.cli._ = [isSingleFolderWorkspaceIdentifier(workspaceToOpen) ? workspaceToOpen : workspaceToOpen.configPath]; + if (isSingleFolderWorkspaceIdentifier(workspaceToOpen)) { + if (workspaceToOpen.scheme === Schemas.file) { + openConfig.cli._ = [workspaceToOpen.fsPath]; + } else { + openConfig.cli['folder-uri'] = [workspaceToOpen.toString()]; + } + } else { + openConfig.cli._ = [workspaceToOpen.configPath]; + } } } // Make sure we are not asked to open a workspace or folder that is already opened - if (openConfig.cli._.some(path => !!findWindowOnWorkspaceOrFolderPath(WindowsManager.WINDOWS, path))) { + if (openConfig.cli._.some(path => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, URI.file(path)))) { openConfig.cli._ = []; } + if (openConfig.cli['folder-uri']) { + const arg = openConfig.cli['folder-uri']; + const folderUris: string[] = typeof arg === 'string' ? [arg] : arg; + if (folderUris.some(uri => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, URI.parse(uri)))) { + openConfig.cli['folder-uri'] = []; + } + } // Open it - this.open({ context: openConfig.context, cli: openConfig.cli, forceNewWindow: true, forceEmpty: openConfig.cli._.length === 0, userEnv: openConfig.userEnv }); + this.open({ context: openConfig.context, cli: openConfig.cli, forceNewWindow: true, forceEmpty: openConfig.cli._.length === 0 && (openConfig.cli['folder-uri'] || []).length === 0, userEnv: openConfig.userEnv }); } private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { - // Build IWindowConfiguration from config and options const configuration: IWindowConfiguration = mixin({}, options.cli); // inherit all properties from CLI configuration.appRoot = this.environmentService.appRoot; @@ -1051,7 +1128,7 @@ export class WindowsManager implements IWindowsMainService { configuration.userEnv = assign({}, this.initialUserEnv, options.userEnv || {}); configuration.isInitialStartup = options.initialStartup; configuration.workspace = options.workspace; - configuration.folderPath = options.folderPath; + configuration.folderUri = options.folderUri; configuration.filesToOpen = options.filesToOpen; configuration.filesToCreate = options.filesToCreate; configuration.filesToDiff = options.filesToDiff; @@ -1141,8 +1218,8 @@ export class WindowsManager implements IWindowsMainService { if (!configuration.extensionDevelopmentPath) { if (configuration.workspace) { configuration.backupPath = this.backupMainService.registerWorkspaceBackupSync(configuration.workspace); - } else if (configuration.folderPath) { - configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderPath); + } else if (configuration.folderUri) { + configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderUri); } else { configuration.backupPath = this.backupMainService.registerEmptyWindowBackupSync(options.emptyWindowBackupFolder); } @@ -1179,8 +1256,8 @@ export class WindowsManager implements IWindowsMainService { } // Known Folder - load from stored settings - if (configuration.folderPath) { - const stateForFolder = this.windowsState.openedWindows.filter(o => isEqual(o.folderPath, configuration.folderPath, !isLinux /* ignorecase */)).map(o => o.uiState); + if (configuration.folderUri) { + const stateForFolder = this.windowsState.openedWindows.filter(o => o.folderUri && isEqual(o.folderUri, configuration.folderUri, hasToIgnoreCase(o.folderUri))).map(o => o.uiState); if (stateForFolder.length) { return stateForFolder[0]; } @@ -1310,6 +1387,10 @@ export class WindowsManager implements IWindowsMainService { return this.workspacesManager.saveAndEnterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); } + enterWorkspace(win: ICodeWindow, path: string): TPromise { + return this.workspacesManager.enterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); + } + createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.workspacesManager.createAndEnterWorkspace(win, folders, path).then(result => this.doEnterWorkspace(win, result)); } @@ -1609,7 +1690,7 @@ class Dialogs { } pickAndOpen(options: INativeOpenDialogOptions): void { - this.getFileOrFolderPaths(options).then(paths => { + this.getFileOrFolderUris(options).then(paths => { const numberOfPaths = paths ? paths.length : 0; // Telemetry @@ -1627,7 +1708,7 @@ class Dialogs { this.windowsMainService.open({ context: OpenContext.DIALOG, cli: this.environmentService.args, - pathsToOpen: paths, + urisToOpen: paths, forceNewWindow: options.forceNewWindow, forceOpenWorkspaceAsFile: options.dialogOptions && !equals(options.dialogOptions.filters, WORKSPACE_FILTER) }); @@ -1635,7 +1716,7 @@ class Dialogs { }); } - private getFileOrFolderPaths(options: IInternalNativeOpenDialogOptions): TPromise { + private getFileOrFolderUris(options: IInternalNativeOpenDialogOptions): TPromise { // Ensure dialog options if (!options.dialogOptions) { @@ -1673,7 +1754,7 @@ class Dialogs { // Remember path in storage for next time this.stateService.setItem(Dialogs.workingDirPickerStorageKey, dirname(paths[0])); - return paths; + return paths.map(path => URI.file(path)); } return void 0; @@ -1775,6 +1856,23 @@ class WorkspacesManager { return this.doSaveAndOpenWorkspace(window, window.openedWorkspace, path); } + enterWorkspace(window: ICodeWindow, path: string): TPromise { + if (!window || !window.win || window.readyState !== ReadyState.READY) { + return TPromise.as(null); // return early if the window is not ready or disposed + } + + return this.isValidTargetWorkspacePath(window, path).then(isValid => { + if (!isValid) { + return TPromise.as(null); // return early if the workspace is not valid + } + + return this.workspacesMainService.resolveWorkspace(path).then(workspace => { + return this.doOpenWorkspace(window, workspace); + }); + }); + + } + createAndEnterWorkspace(window: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { if (!window || !window.win || window.readyState !== ReadyState.READY) { return TPromise.as(null); // return early if the window is not ready or disposed @@ -1826,22 +1924,24 @@ class WorkspacesManager { savePromise = TPromise.as(workspace); } - return savePromise.then(workspace => { - window.focus(); + return savePromise.then(workspace => this.doOpenWorkspace(window, workspace)); + } - // Register window for backups and migrate current backups over - let backupPath: string; - if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath); - } + private doOpenWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult { + window.focus(); - // Update window configuration properly based on transition to workspace - window.config.folderPath = void 0; - window.config.workspace = workspace; - window.config.backupPath = backupPath; + // Register window for backups and migrate current backups over + let backupPath: string; + if (!window.config.extensionDevelopmentPath) { + backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath); + } - return { workspace, backupPath }; - }); + // Update window configuration properly based on transition to workspace + window.config.folderUri = void 0; + window.config.workspace = workspace; + window.config.backupPath = backupPath; + + return { workspace, backupPath }; } pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { @@ -1930,7 +2030,7 @@ class WorkspacesManager { private getUntitledWorkspaceSaveDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { if (workspace) { if (isSingleFolderWorkspaceIdentifier(workspace)) { - return dirname(workspace); + return workspace.scheme === Schemas.file ? dirname(workspace.fsPath) : void 0; } const resolvedWorkspace = this.workspacesMainService.resolveWorkspaceSync(workspace.configPath); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 5dfbc3be44a..3ca3fd9ec94 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -318,7 +318,7 @@ export async function main(argv: string[]): Promise { env }; - if (typeof args['upload-logs'] !== undefined) { + if (typeof args['upload-logs'] !== 'undefined') { options['stdio'] = ['pipe', 'pipe', 'pipe']; } else if (!verbose) { options['stdio'] = 'ignore'; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 396dd120f12..08bd0945583 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -245,7 +245,7 @@ export function main(argv: ParsedArgs): TPromise { if (isBuilt && !extensionDevelopmentPath && !envService.args['disable-telemetry'] && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); + appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey, logService)); } const config: ITelemetryServiceConfig = { diff --git a/src/vs/code/node/windowsFinder.ts b/src/vs/code/node/windowsFinder.ts index d91192d413e..cd9bca97297 100644 --- a/src/vs/code/node/windowsFinder.ts +++ b/src/vs/code/node/windowsFinder.ts @@ -8,12 +8,14 @@ import * as platform from 'vs/base/common/platform'; import * as paths from 'vs/base/common/paths'; import { OpenContext } from 'vs/platform/windows/common/windows'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IResolvedWorkspace } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { Schemas } from 'vs/base/common/network'; +import URI from 'vs/base/common/uri'; +import { hasToIgnoreCase, isEqual } from 'vs/base/common/resources'; export interface ISimpleWindow { openedWorkspace?: IWorkspaceIdentifier; - openedFolderPath?: string; + openedFolderUri?: URI; openedFilePath?: string; extensionDevelopmentPath?: string; lastFocusTime: number; @@ -30,7 +32,7 @@ export interface IBestWindowOrFolderOptions { workspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace; } -export function findBestWindowOrFolderForFile({ windows, newWindow, reuseWindow, context, filePath, workspaceResolver }: IBestWindowOrFolderOptions): W | string { +export function findBestWindowOrFolderForFile({ windows, newWindow, reuseWindow, context, filePath, workspaceResolver }: IBestWindowOrFolderOptions): W { if (!newWindow && filePath && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { const windowOnFilePath = findWindowOnFilePath(windows, filePath, workspaceResolver); if (windowOnFilePath) { @@ -54,9 +56,9 @@ function findWindowOnFilePath(windows: W[], filePath: s } // Then go with single folder windows that are parent of the provided file path - const singleFolderWindowsOnFilePath = windows.filter(window => typeof window.openedFolderPath === 'string' && paths.isEqualOrParent(filePath, window.openedFolderPath, !platform.isLinux /* ignorecase */)); + const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && window.openedFolderUri.scheme === Schemas.file && paths.isEqualOrParent(filePath, window.openedFolderUri.fsPath, !platform.isLinux /* ignorecase */)); if (singleFolderWindowsOnFilePath.length) { - return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderPath.length - b.openedFolderPath.length))[0]; + return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri.path.length - b.openedFolderUri.path.length))[0]; } return null; @@ -73,7 +75,7 @@ export function findWindowOnWorkspace(windows: W[], wor // match on folder if (isSingleFolderWorkspaceIdentifier(workspace)) { - if (typeof window.openedFolderPath === 'string' && (paths.isEqual(window.openedFolderPath, workspace, !platform.isLinux /* ignorecase */))) { + if (window.openedFolderUri && isEqual(window.openedFolderUri, workspace, hasToIgnoreCase(window.openedFolderUri))) { return true; } } @@ -101,16 +103,16 @@ export function findWindowOnExtensionDevelopmentPath(wi })[0]; } -export function findWindowOnWorkspaceOrFolderPath(windows: W[], path: string): W { +export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI): W { return windows.filter(window => { // check for workspace config path - if (window.openedWorkspace && paths.isEqual(window.openedWorkspace.configPath, path, !platform.isLinux /* ignorecase */)) { + if (window.openedWorkspace && isEqual(URI.file(window.openedWorkspace.configPath), uri, !platform.isLinux /* ignorecase */)) { return true; } // check for folder path - if (window.openedFolderPath && paths.isEqual(window.openedFolderPath, path, !platform.isLinux /* ignorecase */)) { + if (window.openedFolderUri && isEqual(window.openedFolderUri, uri, hasToIgnoreCase(uri))) { return true; } diff --git a/src/vs/code/test/node/windowsFinder.test.ts b/src/vs/code/test/node/windowsFinder.test.ts index 084d8546224..b9b70a3503d 100644 --- a/src/vs/code/test/node/windowsFinder.test.ts +++ b/src/vs/code/test/node/windowsFinder.test.ts @@ -10,6 +10,7 @@ import { findBestWindowOrFolderForFile, ISimpleWindow, IBestWindowOrFolderOption import { OpenContext } from 'vs/platform/windows/common/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import URI from 'vs/base/common/uri'; const fixturesFolder = require.toUrl('./fixtures'); @@ -30,10 +31,10 @@ function options(custom?: Partial>): I }; } -const vscodeFolderWindow = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder') }; -const lastActiveWindow = { lastFocusTime: 3, openedFolderPath: null }; -const noVscodeFolderWindow = { lastFocusTime: 2, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder') }; -const windows = [ +const vscodeFolderWindow: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; +const lastActiveWindow: ISimpleWindow = { lastFocusTime: 3, openedFolderUri: null }; +const noVscodeFolderWindow: ISimpleWindow = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; +const windows: ISimpleWindow[] = [ vscodeFolderWindow, lastActiveWindow, noVscodeFolderWindow, @@ -103,7 +104,7 @@ suite('WindowsFinder', () => { windows, filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt') })), vscodeFolderWindow); - const window = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder') }; + const window: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], filePath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt') @@ -111,8 +112,8 @@ suite('WindowsFinder', () => { }); test('More specific existing window wins', () => { - const window = { lastFocusTime: 2, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder') }; - const nestedFolderWindow = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder') }; + const window: ISimpleWindow = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; + const nestedFolderWindow: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window, nestedFolderWindow], filePath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt') @@ -120,7 +121,7 @@ suite('WindowsFinder', () => { }); test('Workspace folder wins', () => { - const window = { lastFocusTime: 1, openedWorkspace: testWorkspace }; + const window: ISimpleWindow = { lastFocusTime: 1, openedWorkspace: testWorkspace }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], filePath: path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt') diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 559d6cdfc1e..28ecaca15e1 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -578,9 +578,9 @@ export interface ICodeEditor extends editorCommon.IEditor { * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. * @param source The source of the call. * @param edits The edits to execute. - * @param endCursoState Cursor state after the edits were applied. + * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursoState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; /** * Execute multiple (concommitent) commands on the editor. @@ -853,4 +853,4 @@ export function getCodeEditor(thing: any): ICodeEditor { } return null; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index c3de31ad7fb..6d9e1f04119 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -20,6 +20,7 @@ import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/commo import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel } from 'vs/editor/common/model'; +import { IPosition } from 'vs/base/browser/ui/contextview/contextview'; export type ServicesAccessor = ServicesAccessor; export type IEditorContributionCtor = IConstructorSignature1; @@ -210,8 +211,14 @@ export function registerLanguageCommand(id: string, handler: (accessor: Services CommandsRegistry.registerCommand(id, (accessor, args) => handler(accessor, args || {})); } -export function registerDefaultLanguageCommand(id: string, handler: (model: ITextModel, position: Position, args: { [n: string]: any }) => any) { - registerLanguageCommand(id, function (accessor, args) { +interface IDefaultArgs { + resource: URI; + position: IPosition; + [name: string]: any; +} + +export function registerDefaultLanguageCommand(id: string, handler: (model: ITextModel, position: Position, args: IDefaultArgs) => any) { + registerLanguageCommand(id, function (accessor, args: IDefaultArgs) { const { resource, position } = args; if (!(resource instanceof URI)) { diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 8bbde13a40f..c31b000de98 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -8,7 +8,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { WorkspaceEdit } from 'vs/editor/common/modes'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from '../editorBrowser'; -import { Selection } from 'vs/editor/common/core/selection'; import { IProgressRunner } from 'vs/platform/progress/common/progress'; export const IBulkEditService = createDecorator('IWorkspaceEditService'); @@ -20,7 +19,6 @@ export interface IBulkEditOptions { } export interface IBulkEditResult { - selection: Selection; ariaSummary: string; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index fcccb8daffc..387e34f62d1 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1356,22 +1356,22 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (this.isSimpleWidget) { commandDelegate = { paste: (source: string, text: string, pasteOnNewLine: boolean, multicursorText: string[]) => { - this.cursor.trigger(source, editorCommon.Handler.Paste, { text, pasteOnNewLine, multicursorText }); + this.trigger(source, editorCommon.Handler.Paste, { text, pasteOnNewLine, multicursorText }); }, type: (source: string, text: string) => { - this.cursor.trigger(source, editorCommon.Handler.Type, { text }); + this.trigger(source, editorCommon.Handler.Type, { text }); }, replacePreviousChar: (source: string, text: string, replaceCharCnt: number) => { - this.cursor.trigger(source, editorCommon.Handler.ReplacePreviousChar, { text, replaceCharCnt }); + this.trigger(source, editorCommon.Handler.ReplacePreviousChar, { text, replaceCharCnt }); }, compositionStart: (source: string) => { - this.cursor.trigger(source, editorCommon.Handler.CompositionStart, undefined); + this.trigger(source, editorCommon.Handler.CompositionStart, undefined); }, compositionEnd: (source: string) => { - this.cursor.trigger(source, editorCommon.Handler.CompositionEnd, undefined); + this.trigger(source, editorCommon.Handler.CompositionEnd, undefined); }, cut: (source: string) => { - this.cursor.trigger(source, editorCommon.Handler.Cut, undefined); + this.trigger(source, editorCommon.Handler.Cut, undefined); } }; } else { diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 9c3fba06d47..dbd07dd8a74 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -31,7 +31,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { Event, Emitter } from 'vs/base/common/event'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { registerThemingParticipant, IThemeService, ITheme, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; -import { scrollbarShadow, diffInserted, diffRemoved, defaultInsertColor, defaultRemoveColor, diffInsertedOutline, diffRemovedOutline } from 'vs/platform/theme/common/colorRegistry'; +import { scrollbarShadow, diffInserted, diffRemoved, defaultInsertColor, defaultRemoveColor, diffInsertedOutline, diffRemovedOutline, diffBorder } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; @@ -1177,7 +1177,7 @@ interface IDataSource { getModifiedEditor(): editorBrowser.ICodeEditor; } -abstract class DiffEditorWidgetStyle extends Disposable { +abstract class DiffEditorWidgetStyle extends Disposable implements IDiffEditorWidgetStyle { _dataSource: IDataSource; _insertColor: Color; @@ -1228,6 +1228,9 @@ abstract class DiffEditorWidgetStyle extends Disposable { protected abstract _getViewZones(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], modifiedForeignVZ: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor, renderIndicators: boolean): IEditorsZones; protected abstract _getOriginalEditorDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorDiffDecorations; protected abstract _getModifiedEditorDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorDiffDecorations; + + public abstract setEnableSplitViewResizing(enableSplitViewResizing: boolean): void; + public abstract layout(): number; } interface IMyViewZone extends editorBrowser.IViewZone { @@ -1529,10 +1532,6 @@ class DiffEdtorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEd this._sash.onDidReset(() => this.onSashReset()); } - public dispose(): void { - super.dispose(); - } - public setEnableSplitViewResizing(enableSplitViewResizing: boolean): void { let newDisableSash = (enableSplitViewResizing === false); if (this._disableSash !== newDisableSash) { @@ -1778,10 +1777,6 @@ class DiffEdtorWidgetInline extends DiffEditorWidgetStyle implements IDiffEditor })); } - public dispose(): void { - super.dispose(); - } - public setEnableSplitViewResizing(enableSplitViewResizing: boolean): void { // Nothing to do.. } @@ -2052,4 +2047,9 @@ registerThemingParticipant((theme, collector) => { if (shadow) { collector.addRule(`.monaco-diff-editor.side-by-side .editor.modified { box-shadow: -6px 0 5px -5px ${shadow}; }`); } + + let border = theme.getColor(diffBorder); + if (border) { + collector.addRule(`.monaco-diff-editor.side-by-side .editor.modified { border-left: 1px solid ${border}; }`); + } }); diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 1ab88ea05ce..deb7e9526c3 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -282,19 +282,19 @@ const editorConfiguration: IConfigurationNode = { 'type': 'number', 'default': EDITOR_MODEL_DEFAULTS.tabSize, 'minimum': 1, - 'description': nls.localize('tabSize', "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `editor.detectIndentation` is on."), + 'description': nls.localize('tabSize', "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on."), 'errorMessage': nls.localize('tabSize.errorMessage', "Expected 'number'. Note that the value \"auto\" has been replaced by the `editor.detectIndentation` setting.") }, 'editor.insertSpaces': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.insertSpaces, - 'description': nls.localize('insertSpaces', "Insert spaces when pressing Tab. This setting is overridden based on the file contents when `editor.detectIndentation` is on."), + 'description': nls.localize('insertSpaces', "Insert spaces when pressing Tab. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on."), 'errorMessage': nls.localize('insertSpaces.errorMessage', "Expected 'boolean'. Note that the value \"auto\" has been replaced by the `editor.detectIndentation` setting.") }, 'editor.detectIndentation': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.detectIndentation, - 'description': nls.localize('detectIndentation', "When opening a file, `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.") + 'description': nls.localize('detectIndentation', "When opening a file, `#editor.tabSize#` and `#editor.insertSpaces#` will be detected based on the file contents.") }, 'editor.roundedSelection': { 'type': 'boolean', @@ -361,17 +361,17 @@ const editorConfiguration: IConfigurationNode = { 'editor.find.seedSearchStringFromSelection': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.find.seedSearchStringFromSelection, - 'description': nls.localize('find.seedSearchStringFromSelection', "Controls if we seed the search string in Find Widget from editor selection") + 'description': nls.localize('find.seedSearchStringFromSelection', "Controls whether the search string in the Find Widget is seeded from the editor selection.") }, 'editor.find.autoFindInSelection': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.find.autoFindInSelection, - 'description': nls.localize('find.autoFindInSelection', "Controls if Find in Selection flag is turned on when multiple characters or lines of text are selected in the editor") + 'description': nls.localize('find.autoFindInSelection', "Controls whether the Find in Selection flag is turned on when multiple characters or lines of text are selected in the editor.") }, 'editor.find.globalFindClipboard': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.find.globalFindClipboard, - 'description': nls.localize('find.globalFindClipboard', "Controls if the Find Widget should read or modify the shared find clipboard on macOS"), + 'description': nls.localize('find.globalFindClipboard', "Controls whether the Find Widget should read or modify the shared find clipboard on macOS."), 'included': platform.isMacintosh }, 'editor.wordWrap': { @@ -385,14 +385,14 @@ const editorConfiguration: IConfigurationNode = { comment: [ '- `editor.wordWrapColumn` refers to a different setting and should not be localized.' ] - }, "Lines will wrap at `editor.wordWrapColumn`."), + }, "Lines will wrap at `#editor.wordWrapColumn#`."), nls.localize({ key: 'wordWrap.bounded', comment: [ '- viewport means the edge of the visible window size.', '- `editor.wordWrapColumn` refers to a different setting and should not be localized.' ] - }, "Lines will wrap at the minimum of viewport and `editor.wordWrapColumn`."), + }, "Lines will wrap at the minimum of viewport and `#editor.wordWrapColumn#`."), ], 'default': EDITOR_DEFAULTS.wordWrap, 'description': nls.localize({ @@ -401,7 +401,7 @@ const editorConfiguration: IConfigurationNode = { '- \'off\', \'on\', \'wordWrapColumn\' and \'bounded\' refer to values the setting can take and should not be localized.', '- `editor.wordWrapColumn` refers to a different setting and should not be localized.' ] - }, "Controls how lines should wrap. Can be:\n - 'off' (disable wrapping),\n - 'on' (viewport wrapping),\n - 'wordWrapColumn' (wrap at `editor.wordWrapColumn`) or\n - 'bounded' (wrap at minimum of viewport and `editor.wordWrapColumn`).") + }, "Controls how lines should wrap.") }, 'editor.wordWrapColumn': { 'type': 'integer', @@ -413,7 +413,7 @@ const editorConfiguration: IConfigurationNode = { '- `editor.wordWrap` refers to a different setting and should not be localized.', '- \'wordWrapColumn\' and \'bounded\' refer to values the different setting can take and should not be localized.' ] - }, "Controls the wrapping column of the editor when `editor.wordWrap` is 'wordWrapColumn' or 'bounded'.") + }, "Controls the wrapping column of the editor when `#editor.wordWrap#` is `wordWrapColumn` or `bounded`.") }, 'editor.wrappingIndent': { 'type': 'string', @@ -440,7 +440,7 @@ const editorConfiguration: IConfigurationNode = { '- `ctrlCmd` refers to a value the setting can take and should not be localized.', '- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.' ] - }, "The modifier to be used to add multiple cursors with the mouse. `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier.") + }, "The modifier to be used to add multiple cursors with the mouse. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier. [Read more](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).") }, 'editor.multiCursorMergeOverlapping': { 'type': 'boolean', @@ -495,7 +495,7 @@ const editorConfiguration: IConfigurationNode = { 'editor.formatOnType': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.formatOnType, - 'description': nls.localize('formatOnType', "Controls if the editor should automatically format the line after typing") + 'description': nls.localize('formatOnType', "Controls if the editor should automatically format the line after typing.") }, 'editor.formatOnPaste': { 'type': 'boolean', @@ -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, @@ -608,17 +613,17 @@ const editorConfiguration: IConfigurationNode = { 'type': 'string', 'enum': ['block', 'block-outline', 'line', 'line-thin', 'underline', 'underline-thin'], 'default': editorOptions.cursorStyleToString(EDITOR_DEFAULTS.viewInfo.cursorStyle), - 'description': nls.localize('cursorStyle', "Controls the cursor style, accepted values are 'block', 'block-outline', 'line', 'line-thin', 'underline' and 'underline-thin'") + 'description': nls.localize('cursorStyle', "Controls the cursor style.") }, 'editor.cursorWidth': { 'type': 'integer', 'default': EDITOR_DEFAULTS.viewInfo.cursorWidth, - 'description': nls.localize('cursorWidth', "Controls the width of the cursor when editor.cursorStyle is set to 'line'") + 'description': nls.localize('cursorWidth', "Controls the width of the cursor when `#editor.cursorStyle#` is set to `line`.") }, 'editor.fontLigatures': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.fontLigatures, - 'description': nls.localize('fontLigatures', "Enables font ligatures") + 'description': nls.localize('fontLigatures', "Enables font ligatures.") }, 'editor.hideCursorInOverviewRuler': { 'type': 'boolean', @@ -628,8 +633,13 @@ const editorConfiguration: IConfigurationNode = { 'editor.renderWhitespace': { 'type': 'string', 'enum': ['none', 'boundary', 'all'], + 'enumDescriptions': [ + '', + nls.localize('renderWhiteSpace.boundary', "Render whitespace characters except for single spaces between words."), + '' + ], default: EDITOR_DEFAULTS.viewInfo.renderWhitespace, - description: nls.localize('renderWhitespace', "Controls how the editor should render whitespace characters, possibilities are 'none', 'boundary', and 'all'. The 'boundary' option does not render single spaces between words.") + description: nls.localize('renderWhitespace', "Controls how the editor should render whitespace characters.") }, 'editor.renderControlCharacters': { 'type': 'boolean', diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2ea6bc0e971..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; } /** @@ -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/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index bb9c031c095..039f6706bd2 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -607,7 +607,7 @@ export namespace CursorMove { \`\`\` 'left', 'right', 'up', 'down' 'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter' - 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter', + 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter' 'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside' \`\`\` * 'by': Unit to move. Default is computed based on 'to' value. diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 94f3bf63ed2..02c236a53c2 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -554,7 +554,7 @@ export interface DefinitionProvider { /** * Provide the definition of the symbol at the given position and document. */ - provideDefinition(model: model.ITextModel, position: Position, token: CancellationToken): DefinitionLink | Thenable; + provideDefinition(model: model.ITextModel, position: Position, token: CancellationToken): Definition | DefinitionLink[] | Thenable; } /** @@ -565,7 +565,7 @@ export interface ImplementationProvider { /** * Provide the implementation of the symbol at the given position and document. */ - provideImplementation(model: model.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideImplementation(model: model.ITextModel, position: Position, token: CancellationToken): Definition | DefinitionLink[] | Thenable; } /** @@ -576,7 +576,7 @@ export interface TypeDefinitionProvider { /** * Provide the type definition of the symbol at the given position and document. */ - provideTypeDefinition(model: model.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideTypeDefinition(model: model.ITextModel, position: Position, token: CancellationToken): Definition | DefinitionLink[] | Thenable; } /** @@ -646,7 +646,7 @@ export const symbolKindToCssClass = (function () { _fromMapping[SymbolKind.TypeParameter] = 'type-parameter'; return function toCssClassName(kind: SymbolKind): string { - return _fromMapping[kind] || 'property'; + return `symbol-icon ${_fromMapping[kind] || 'property'}`; }; })(); @@ -906,7 +906,7 @@ export function isResourceTextEdit(thing: any): thing is ResourceTextEdit { export interface ResourceFileEdit { oldUri: URI; newUri: URI; - options: { overwrite?: boolean, ignoreIfExists?: boolean, recursive?: boolean }; + options: { overwrite?: boolean, ignoreIfNotExists?: boolean, ignoreIfExists?: boolean, recursive?: boolean }; } export interface ResourceTextEdit { diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index a184362fd69..842df862701 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -13,7 +13,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { createScopedLineTokens } from 'vs/editor/common/modes/supports'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; @@ -189,14 +189,12 @@ export class LanguageConfigurationRegistryImpl { let current = new RichEditSupport(languageIdentifier, previous, configuration); this._entries[languageIdentifier.id] = current; this._onDidChange.fire({ languageIdentifier }); - return { - dispose: () => { - if (this._entries[languageIdentifier.id] === current) { - this._entries[languageIdentifier.id] = previous; - this._onDidChange.fire({ languageIdentifier }); - } + return toDisposable(() => { + if (this._entries[languageIdentifier.id] === current) { + this._entries[languageIdentifier.id] = previous; + this._onDidChange.fire({ languageIdentifier }); } - }; + }); } private _getRichEditSupport(languageId: LanguageId): RichEditSupport { diff --git a/src/vs/editor/common/modes/languageFeatureRegistry.ts b/src/vs/editor/common/modes/languageFeatureRegistry.ts index c5e4e7010d8..9dfd73755f2 100644 --- a/src/vs/editor/common/modes/languageFeatureRegistry.ts +++ b/src/vs/editor/common/modes/languageFeatureRegistry.ts @@ -6,7 +6,7 @@ 'use strict'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; import { LanguageSelector, score } from 'vs/editor/common/modes/languageSelector'; import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; @@ -54,19 +54,17 @@ export default class LanguageFeatureRegistry { this._lastCandidate = undefined; this._onDidChange.fire(this._entries.length); - return { - dispose: () => { - if (entry) { - let idx = this._entries.indexOf(entry); - if (idx >= 0) { - this._entries.splice(idx, 1); - this._lastCandidate = undefined; - this._onDidChange.fire(this._entries.length); - entry = undefined; - } + return toDisposable(() => { + if (entry) { + let idx = this._entries.indexOf(entry); + if (idx >= 0) { + this._entries.splice(idx, 1); + this._lastCandidate = undefined; + this._onDidChange.fire(this._entries.length); + entry = undefined; } } - }; + }); } has(model: ITextModel): boolean { diff --git a/src/vs/editor/common/modes/tokenizationRegistry.ts b/src/vs/editor/common/modes/tokenizationRegistry.ts index 660ef056881..dd13f213e76 100644 --- a/src/vs/editor/common/modes/tokenizationRegistry.ts +++ b/src/vs/editor/common/modes/tokenizationRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ColorId, ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent } from 'vs/editor/common/modes'; import { Color } from 'vs/base/common/color'; @@ -33,15 +33,13 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { public register(language: string, support: ITokenizationSupport): IDisposable { this._map[language] = support; this.fire([language]); - return { - dispose: () => { - if (this._map[language] !== support) { - return; - } - delete this._map[language]; - this.fire([language]); + return toDisposable(() => { + if (this._map[language] !== support) { + return; } - }; + delete this._map[language]; + this.fire([language]); + }); } public get(language: string): ITokenizationSupport { diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index f78056b1667..d478fef73c6 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -16,13 +16,13 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { MirrorTextModel as BaseMirrorModel, IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { IInplaceReplaceSupportResult, ILink, ISuggestResult, ISuggestion, TextEdit } from 'vs/editor/common/modes'; -import { computeLinks } from 'vs/editor/common/modes/linkComputer'; +import { computeLinks, ILinkComputerTarget } from 'vs/editor/common/modes/linkComputer'; import { BasicInplaceReplace } from 'vs/editor/common/modes/supports/inplaceReplaceSupport'; import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { IWordAtPosition, EndOfLineSequence } from 'vs/editor/common/model'; import { globals } from 'vs/base/common/platform'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; export interface IMirrorModel { readonly uri: URI; @@ -50,7 +50,7 @@ export interface IRawModelData { /** * @internal */ -export interface ICommonModel { +export interface ICommonModel extends ILinkComputerTarget, IMirrorModel { uri: URI; version: number; eol: string; @@ -59,7 +59,7 @@ export interface ICommonModel { getLinesContent(): string[]; getLineCount(): number; getLineContent(lineNumber: number): string; - createWordIterator(wordDefinition: RegExp): IIterator; + createWordIterator(wordDefinition: RegExp): Iterator; getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition; getValueInRange(range: IRange): string; getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range; @@ -147,7 +147,7 @@ class MirrorModel extends BaseMirrorModel implements ICommonModel { }; } - public createWordIterator(wordDefinition: RegExp): IIterator { + public createWordIterator(wordDefinition: RegExp): Iterator { let obj = { done: false, value: '' diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index efbf104f703..b03f956e982 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -5,7 +5,7 @@ 'use strict'; import { IntervalTimer, ShallowCancelThenPromise, wireCancellationToken } from 'vs/base/common/async'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { SimpleWorkerClient, logOnceWebWorkerWarning } from 'vs/base/common/worker/simpleWorker'; @@ -285,11 +285,9 @@ class EditorModelManager extends Disposable { toDispose.push(model.onWillDispose(() => { this._stopModelSync(modelUrl); })); - toDispose.push({ - dispose: () => { - this._proxy.acceptRemovedModel(modelUrl); - } - }); + toDispose.push(toDisposable(() => { + this._proxy.acceptRemovedModel(modelUrl); + })); this._syncedModels[modelUrl] = toDispose; } diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index d28e49abcfe..a406a7bddcf 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -9,7 +9,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import * as errors from 'vs/base/common/errors'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollType } from 'vs/editor/common/editorCommon'; export const enum ViewEventType { @@ -354,17 +354,15 @@ export class ViewEventEmitter extends Disposable { public addEventListener(listener: (events: ViewEvent[]) => void): IDisposable { this._listeners.push(listener); - return { - dispose: () => { - let listeners = this._listeners; - for (let i = 0, len = listeners.length; i < len; i++) { - if (listeners[i] === listener) { - listeners.splice(i, 1); - break; - } + return toDisposable(() => { + let listeners = this._listeners; + for (let i = 0, len = listeners.length; i < len; i++) { + if (listeners[i] === listener) { + listeners.splice(i, 1); + break; } } - }; + }); } } diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 9a6ea89f441..6fc472644dd 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isFalsyOrEmpty, mergeSort, flatten } from 'vs/base/common/arrays'; +import { flatten, isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays'; import { asWinJsPromise } from 'vs/base/common/async'; -import { illegalArgument, onUnexpectedExternalError, isPromiseCanceledError } from 'vs/base/common/errors'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeAction, CodeActionProviderRegistry, CodeActionContext, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; 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, token: CancellationToken = CancellationToken.None): Promise { const codeActionContext: CodeActionContext = { diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index c86a8b15608..53870292126 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancelablePromise } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeAction } from 'vs/editor/common/modes'; @@ -18,14 +21,11 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IProgressService } from 'vs/platform/progress/common/progress'; import { CodeActionModel, CodeActionsComputeEvent, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger'; import { CodeActionContextMenu } from './codeActionWidget'; 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( @@ -88,7 +88,7 @@ export class QuickFixController implements IEditorContribution { this._activeRequest = e.actions; } - if (e && e.trigger.filter && e.trigger.filter.kind) { + if (e && e.actions && e.trigger.filter && e.trigger.filter.kind) { // Triggered for specific scope // Apply if we only have one action or requested autoApply, otherwise show menu e.actions.then(fixes => { @@ -122,7 +122,9 @@ export class QuickFixController implements IEditorContribution { } private _handleLightBulbSelect(coords: { x: number, y: number }): void { - this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords); + if (this._lightBulbWidget.model.actions) { + this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords); + } } public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): Thenable { @@ -195,7 +197,7 @@ export class QuickFixAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available")); } } @@ -248,7 +250,7 @@ export class CodeActionCommand extends EditorCommand { }); } - public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { + public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { const args = CodeActionCommandArgs.fromUser(userArg); return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), { kind: args.kind, includeSourceActions: true }, args.apply); } @@ -282,7 +284,7 @@ export class RefactorAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.refactor.noneMessage', "No refactorings available"), { kind: CodeActionKind.Refactor }, @@ -311,7 +313,7 @@ export class SourceAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.source.noneMessage', "No source actions available"), { kind: CodeActionKind.Source, includeSourceActions: true }, @@ -338,7 +340,7 @@ export class OrganizeImportsAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.organize.noneMessage', "No organize imports action available"), { kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true }, diff --git a/src/vs/editor/contrib/codeAction/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/codeActionContributions.ts index 8653b935470..4f7315db0f8 100644 --- a/src/vs/editor/contrib/codeAction/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/codeActionContributions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { SourceAction, QuickFixController, QuickFixAction, CodeActionCommand, RefactorAction, OrganizeImportsAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; registerEditorContribution(QuickFixController); diff --git a/src/vs/editor/contrib/codeAction/codeActionModel.ts b/src/vs/editor/contrib/codeAction/codeActionModel.ts index 98211ae24dd..e5ebc28760d 100644 --- a/src/vs/editor/contrib/codeAction/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/codeActionModel.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, debounceEvent } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -14,10 +15,9 @@ import { Selection } from 'vs/editor/common/core/selection'; import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IProgressService } from 'vs/platform/progress/common/progress'; 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', ''); diff --git a/src/vs/editor/contrib/codeAction/codeActionTrigger.ts b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts index 4f3c53b5dc9..2e704921dd6 100644 --- a/src/vs/editor/contrib/codeAction/codeActionTrigger.ts +++ b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts @@ -9,6 +9,7 @@ export class CodeActionKind { private static readonly sep = '.'; public static readonly Empty = new CodeActionKind(''); + public static readonly QuickFix = new CodeActionKind('quickfix'); public static readonly Refactor = new CodeActionKind('refactor'); public static readonly Source = new CodeActionKind('source'); public static readonly SourceOrganizeImports = new CodeActionKind('source.organizeImports'); diff --git a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts index 0fd9d8270f1..e6272677ea4 100644 --- a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts @@ -7,12 +7,12 @@ import * as dom from 'vs/base/browser/dom'; import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./lightBulbWidget'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { CodeActionsComputeEvent } from './codeActionModel'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; +import { CodeActionsComputeEvent } from './codeActionModel'; export class LightBulbWidget implements IDisposable, IContentWidget { diff --git a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts index c2cc03ff9da..a2105b6928c 100644 --- a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts @@ -5,14 +5,14 @@ 'use strict'; import * as assert from 'assert'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, ResourceTextEdit, CodeAction, CodeActionContext } from 'vs/editor/common/modes'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { CodeAction, CodeActionContext, CodeActionProvider, CodeActionProviderRegistry, Command, LanguageIdentifier, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; -import { MarkerSeverity, IMarkerData } from 'vs/platform/markers/common/markers'; +import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; suite('CodeAction', () => { diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index be43648e661..23fde7dc577 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -5,20 +5,20 @@ 'use strict'; -import { RunOnceScheduler, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import * as editorCommon from 'vs/editor/common/editorCommon'; -import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ICodeLensData, getCodeLensData } from './codelens'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; -import { CodeLens, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget'; +import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; +import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes'; +import { CodeLens, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; +import { getCodeLensData, ICodeLensData } from './codelens'; export class CodeLensContribution implements editorCommon.IEditorContribution { @@ -167,22 +167,20 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._localToDispose.push(this._editor.onDidLayoutChange(e => { this._detectVisibleLenses.schedule(); })); - this._localToDispose.push({ - dispose: () => { - if (this._editor.getModel()) { - const scrollState = StableEditorScrollState.capture(this._editor); - this._editor.changeDecorations((changeAccessor) => { - this._editor.changeViewZones((accessor) => { - this._disposeAllLenses(changeAccessor, accessor); - }); + this._localToDispose.push(toDisposable(() => { + if (this._editor.getModel()) { + const scrollState = StableEditorScrollState.capture(this._editor); + this._editor.changeDecorations((changeAccessor) => { + this._editor.changeViewZones((accessor) => { + this._disposeAllLenses(changeAccessor, accessor); }); - scrollState.restore(this._editor); - } else { - // No accessors available - this._disposeAllLenses(null, null); - } + }); + scrollState.restore(this._editor); + } else { + // No accessors available + this._disposeAllLenses(null, null); } - }); + })); scheduler.schedule(); } diff --git a/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x.svg b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x.svg new file mode 100644 index 00000000000..d9fd295d0b6 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x_darkp.svg new file mode 100644 index 00000000000..48e8c5a3838 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Class_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Class_16x.svg new file mode 100644 index 00000000000..e553c3633e5 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Class_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Class_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Class_16x_darkp.svg new file mode 100644 index 00000000000..746e7dfc6d8 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Class_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x.svg b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x.svg new file mode 100644 index 00000000000..2af5cc6faef --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x_darkp.svg new file mode 100644 index 00000000000..a2df3032cb1 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Document_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Document_16x.svg new file mode 100644 index 00000000000..7b36178ab46 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Document_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Document_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Document_16x_darkp.svg new file mode 100644 index 00000000000..bced3a467ee --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Document_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Enumerator_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Enumerator_16x.svg new file mode 100755 index 00000000000..e4a9551fd5a --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Enumerator_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Enumerator_inverse_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Enumerator_inverse_16x.svg new file mode 100755 index 00000000000..d8e9f4f107a --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Enumerator_inverse_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Field_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Field_16x.svg new file mode 100644 index 00000000000..e1b5aa5e31d --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Field_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Field_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Field_16x_darkp.svg new file mode 100644 index 00000000000..e1b5aa5e31d --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Field_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Indexer_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x.svg new file mode 100644 index 00000000000..ff55f31ffa3 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Indexer_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x_darkp.svg new file mode 100644 index 00000000000..2f3788e7730 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x.svg b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x.svg new file mode 100644 index 00000000000..7a80c7fe260 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x_darkp.svg new file mode 100644 index 00000000000..ef98b5133fd --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Interface_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Interface_16x.svg new file mode 100644 index 00000000000..0c08c8d50af --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Interface_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Interface_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Interface_16x_darkp.svg new file mode 100644 index 00000000000..0c08c8d50af --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Interface_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode.svg b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode.svg new file mode 100644 index 00000000000..e78894b6c63 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode_inverse.svg new file mode 100644 index 00000000000..44a44b489d1 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Method_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Method_16x.svg new file mode 100644 index 00000000000..e1b587f9cc0 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Method_16x.svg @@ -0,0 +1 @@ +Method_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Method_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Method_16x_darkp.svg new file mode 100644 index 00000000000..0b7dd26efd3 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Method_16x_darkp.svg @@ -0,0 +1 @@ +Method_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Namespace_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x.svg new file mode 100644 index 00000000000..772b9152cb5 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Namespace_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x_darkp.svg new file mode 100644 index 00000000000..dc052a068ca --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Numeric_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x.svg new file mode 100644 index 00000000000..ac848f89b8c --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Numeric_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x_darkp.svg new file mode 100644 index 00000000000..4144eea0c06 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Property_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Property_16x.svg new file mode 100644 index 00000000000..cac629e1132 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Property_16x.svg @@ -0,0 +1 @@ +Property_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Property_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Property_16x_darkp.svg new file mode 100644 index 00000000000..bad83c9a321 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Property_16x_darkp.svg @@ -0,0 +1 @@ +Property_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Snippet_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x.svg new file mode 100644 index 00000000000..640c247786e --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Snippet_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x_darkp.svg new file mode 100644 index 00000000000..0fb4b8bc9e3 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/String_16x.svg b/src/vs/editor/contrib/documentSymbols/media/String_16x.svg new file mode 100644 index 00000000000..880d50dd03f --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/String_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/String_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/String_16x_darkp.svg new file mode 100644 index 00000000000..de3ea3b37eb --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/String_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/media/symbol-icons.css b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css index 2369d98e0ed..3232bc55f4f 100644 --- a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css +++ b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css @@ -11,171 +11,257 @@ min-width: 16px; } -.monaco-workbench .symbol-icon.constant { - background-image: url('Constant_16x.svg'); +/* default icons */ +.monaco-workbench .symbol-icon { + background-image: url('Field_16x.svg'); background-repeat: no-repeat; - background-position: 0 -2px; +} +.vs-dark .monaco-workbench .symbol-icon, +.hc-black .monaco-workbench .symbol-icon { + background-image: url('Field_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.constant, .hc-black .monaco-workbench .symbol-icon.constant { +/* constant */ +.monaco-workbench .symbol-icon.constant { + background-image: url('Constant_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.constant, +.hc-black .monaco-workbench .symbol-icon.constant { background-image: url('Constant_16x_inverse.svg'); } -.monaco-workbench .symbol-icon.enum-member { - background-image: url('EnumItem_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; +/* enum */ +.monaco-workbench .symbol-icon.enum { + background-image: url('Enumerator_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.enum, +.hc-black .monaco-workbench .symbol-icon.enum { + background-image: url('Enumerator_inverse_16x.svg'); } -.vs-dark .monaco-workbench .symbol-icon.enum-member, .hc-black .monaco-workbench .symbol-icon.enum-member { +/* enum-member */ +.monaco-workbench .symbol-icon.enum-member { + background-image: url('EnumItem_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.enum-member, +.hc-black .monaco-workbench .symbol-icon.enum-member { background-image: url('EnumItem_inverse_16x.svg'); } +/* struct */ .monaco-workbench .symbol-icon.struct { background-image: url('Structure_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; } - -.vs-dark .monaco-workbench .symbol-icon.struct, .hc-black .monaco-workbench .symbol-icon.struct { +.vs-dark .monaco-workbench .symbol-icon.struct, +.hc-black .monaco-workbench .symbol-icon.struct { background-image: url('Structure_16x_vscode_inverse.svg'); } +/* event */ .monaco-workbench .symbol-icon.event { background-image: url('Event_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; } - -.vs-dark .monaco-workbench .symbol-icon.event, .hc-black .monaco-workbench .symbol-icon.event { +.vs-dark .monaco-workbench .symbol-icon.event, +.hc-black .monaco-workbench .symbol-icon.event { background-image: url('Event_16x_vscode_inverse.svg'); } +/* operator */ .monaco-workbench .symbol-icon.operator { background-image: url('Operator_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; } - -.vs-dark .monaco-workbench .symbol-icon.operator, .hc-black .monaco-workbench .symbol-icon.operator { +.vs-dark .monaco-workbench .symbol-icon.operator, +.hc-black .monaco-workbench .symbol-icon.operator { background-image: url('Operator_16x_vscode_inverse.svg'); } +/* type paramter */ .monaco-workbench .symbol-icon.type-parameter { background-image: url('Template_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; } - -.vs-dark .monaco-workbench .symbol-icon.type-parameter, .hc-black .monaco-workbench .symbol-icon.type-parameter { +.vs-dark .monaco-workbench .symbol-icon.type-parameter, +.hc-black .monaco-workbench .symbol-icon.type-parameter { background-image: url('Template_16x_vscode_inverse.svg'); } -.monaco-workbench .symbol-icon.method, .monaco-workbench .symbol-icon.function, .monaco-workbench .symbol-icon.constructor, .monaco-workbench .symbol-icon.field, .monaco-workbench .symbol-icon.variable, .monaco-workbench .symbol-icon.class, .monaco-workbench .symbol-icon.interface, .monaco-workbench .symbol-icon.object, .monaco-workbench .symbol-icon.namespace, .monaco-workbench .symbol-icon.package, .monaco-workbench .symbol-icon.module, .monaco-workbench .symbol-icon.property, .monaco-workbench .symbol-icon.enum, .monaco-workbench .symbol-icon.key, .monaco-workbench .symbol-icon.string, .monaco-workbench .symbol-icon.rule, .monaco-workbench .symbol-icon.file, .monaco-workbench .symbol-icon.array, .monaco-workbench .symbol-icon.number, .monaco-workbench .symbol-icon.null, .monaco-workbench .symbol-icon.boolean { - background-image: url('symbol-sprite.svg'); - background-repeat: no-repeat; +/* boolean, null */ +.monaco-workbench .symbol-icon.boolean { + background-image: url('BooleanData_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.boolean, +.hc-black .monaco-workbench .symbol-icon.boolean { + background-image: url('BooleanData_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.method, .vs .monaco-workbench .symbol-icon.function, .vs .monaco-workbench .symbol-icon.constructor { - background-position: 0 -4px; +/* null */ +.monaco-workbench .symbol-icon.null { + background-image: url('BooleanData_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.null, +.hc-black .monaco-workbench .symbol-icon.null { + background-image: url('BooleanData_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.field, .vs .monaco-workbench .symbol-icon.variable { - background-position: -22px -4px; +/* class */ +.monaco-workbench .symbol-icon.class { + background-image: url('Class_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.class, +.hc-black .monaco-workbench .symbol-icon.class { + background-image: url('Class_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.class { - background-position: -43px -3px; +/* file */ +.monaco-workbench .symbol-icon.file { + background-image: url('Document_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.file, +.hc-black .monaco-workbench .symbol-icon.file { + background-image: url('Document_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.interface { - background-position: -63px -4px; +/* field */ +.monaco-workbench .symbol-icon.field { + background-image: url('Field_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.field, +.hc-black .monaco-workbench .symbol-icon.field { + background-image: url('Field_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.object, .vs .monaco-workbench .symbol-icon.namespace, .vs .monaco-workbench .symbol-icon.package, .vs .monaco-workbench .symbol-icon.module { - background-position: -82px -4px; +/* variable */ +.monaco-workbench .symbol-icon.variable { + background-image: url('LocalVariable_16x_vscode.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.variable, +.hc-black .monaco-workbench .symbol-icon.variable { + background-image: url('LocalVariable_16x_vscode_inverse.svg'); } -.vs .monaco-workbench .symbol-icon.property { - background-position: -102px -3px; +/* array */ +.monaco-workbench .symbol-icon.array { + background-image: url('Indexer_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.array, +.hc-black .monaco-workbench .symbol-icon.array { + background-image: url('Indexer_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.enum { - background-position: -122px -3px; +/* keyword */ +/* todo@joh not used? */ +.monaco-workbench .symbol-icon.keyword { + background-image: url('IntelliSenseKeyword_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.keyword, +.hc-black .monaco-workbench .symbol-icon.keyword { + background-image: url('IntelliSenseKeyword_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.key, .vs .monaco-workbench .symbol-icon.string { - background-position: -202px -3px; +/* interface */ +.monaco-workbench .symbol-icon.interface { + background-image: url('Interface_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.interface, +.hc-black .monaco-workbench .symbol-icon.interface { + background-image: url('Interface_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.rule { - background-position: -242px -4px; +/* method */ +.monaco-workbench .symbol-icon.method { + background-image: url('Method_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.method, +.hc-black .monaco-workbench .symbol-icon.method { + background-image: url('Method_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.file { - background-position: -262px -4px; +/* function */ +.monaco-workbench .symbol-icon.function { + background-image: url('Method_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.function, +.hc-black .monaco-workbench .symbol-icon.function { + background-image: url('Method_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.array { - background-position: -302px -4px; +/* object */ +.monaco-workbench .symbol-icon.object { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.object, +.hc-black .monaco-workbench .symbol-icon.object { + background-image: url('Namespace_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.number { - background-position: -322px -4px; +/* namespace */ +.monaco-workbench .symbol-icon.namespace { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.namespace, +.hc-black .monaco-workbench .symbol-icon.namespace { + background-image: url('Namespace_16x_darkp.svg'); } -.vs .monaco-workbench .symbol-icon.null, .vs .monaco-workbench .symbol-icon.boolean { - background-position: -343px -4px; +/* package */ +.monaco-workbench .symbol-icon.package { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.package, +.hc-black .monaco-workbench .symbol-icon.package { + background-image: url('Namespace_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.method, .vs-dark .monaco-workbench .symbol-icon.function, .vs-dark .monaco-workbench .symbol-icon.constructor, .hc-black .monaco-workbench .symbol-icon.method, .hc-black .monaco-workbench .symbol-icon.function, .hc-black .monaco-workbench .symbol-icon.constructor { - background-position: 0 -24px; +/* module */ +.monaco-workbench .symbol-icon.module { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.module, +.hc-black .monaco-workbench .symbol-icon.module { + background-image: url('Namespace_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.field, .hc-black .monaco-workbench .symbol-icon.field, .vs-dark .monaco-workbench .symbol-icon.variable, .hc-black .monaco-workbench .symbol-icon.variable { - background-position: -22px -24px; +/* number */ +.monaco-workbench .symbol-icon.number { + background-image: url('Numeric_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.number, +.hc-black .monaco-workbench .symbol-icon.number { + background-image: url('Numeric_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.class, .hc-black .monaco-workbench .symbol-icon.class { - background-position: -43px -23px; +/* property */ +.monaco-workbench .symbol-icon.property { + background-image: url('Property_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.property, +.hc-black .monaco-workbench .symbol-icon.property { + background-image: url('Property_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.interface, .hc-black .monaco-workbench .symbol-icon.interface { - background-position: -63px -24px; +/* snippet */ +/* todo@joh unused? */ +.monaco-workbench .symbol-icon.snippet { + background-image: url('Snippet_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.snippet, +.hc-black .monaco-workbench .symbol-icon.snippet { + background-image: url('Snippet_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.object, .vs-dark .monaco-workbench .symbol-icon.namespace, .vs-dark .monaco-workbench .symbol-icon.package, .vs-dark .monaco-workbench .symbol-icon.module, .hc-black .monaco-workbench .symbol-icon.object, .hc-black .monaco-workbench .symbol-icon.namespace, .hc-black .monaco-workbench .symbol-icon.package, .hc-black .monaco-workbench .symbol-icon.module { - background-position: -82px -24px; +/* string */ +.monaco-workbench .symbol-icon.string { + background-image: url('String_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.string, +.hc-black .monaco-workbench .symbol-icon.string { + background-image: url('String_16x_darkp.svg'); } -.vs-dark .monaco-workbench .symbol-icon.property, .hc-black .monaco-workbench .symbol-icon.property { - background-position: -102px -23px; +/* key */ +.monaco-workbench .symbol-icon.key { + background-image: url('String_16x.svg'); } - -.vs-dark .monaco-workbench .symbol-icon.key, .vs-dark .monaco-workbench .symbol-icon.string, .hc-black .monaco-workbench .symbol-icon.key, .hc-black .monaco-workbench .symbol-icon.string { - background-position: -202px -23px; -} - -.vs-dark .monaco-workbench .symbol-icon.enum, .hc-black .monaco-workbench .symbol-icon.enum { - background-position: -122px -23px; -} - -.vs-dark .monaco-workbench .symbol-icon.rule, .hc-black .monaco-workbench .symbol-icon.rule { - background-position: -242px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.file, .hc-black .monaco-workbench .symbol-icon.file { - background-position: -262px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.array, .hc-black .monaco-workbench .symbol-icon.array { - background-position: -302px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.number, .hc-black .monaco-workbench .symbol-icon.number { - background-position: -322px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.null, .vs-dark .monaco-workbench .symbol-icon.boolean, .hc-black .monaco-workbench .symbol-icon.null, .hc-black .monaco-workbench .symbol-icon.boolean { - background-position: -342px -24px; +.vs-dark .monaco-workbench .symbol-icon.key, +.hc-black .monaco-workbench .symbol-icon.key { + background-image: url('String_16x_darkp.svg'); } diff --git a/src/vs/editor/contrib/documentSymbols/media/symbol-sprite.svg b/src/vs/editor/contrib/documentSymbols/media/symbol-sprite.svg deleted file mode 100644 index ee9a63dcf6f..00000000000 --- a/src/vs/editor/contrib/documentSymbols/media/symbol-sprite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index a2a8a2447f8..28db38d0d47 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -9,7 +9,7 @@ import { ITextModel } from 'vs/editor/common/model'; 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'; -import { first, size } from 'vs/base/common/collections'; +import { first, size, forEach } from 'vs/base/common/collections'; import { isFalsyOrEmpty, binarySearch, coalesce } from 'vs/base/common/arrays'; import { commonPrefixLength } from 'vs/base/common/strings'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; @@ -18,10 +18,13 @@ import { LRUCache } from 'vs/base/common/map'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export abstract class TreeElement { + abstract id: string; abstract children: { [id: string]: TreeElement }; abstract parent: TreeElement | any; + abstract adopt(newParent: TreeElement): TreeElement; + static findId(candidate: DocumentSymbol | string, container: TreeElement): string { // complex id-computation which contains the origin/extension, // the parent path, and some dedupe logic when names collide @@ -85,6 +88,12 @@ export class OutlineElement extends TreeElement { ) { super(); } + + adopt(parent: OutlineModel | OutlineGroup | OutlineElement): OutlineElement { + let res = new OutlineElement(this.id, parent, this.symbol); + forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res)); + return res; + } } export class OutlineGroup extends TreeElement { @@ -100,6 +109,12 @@ export class OutlineGroup extends TreeElement { super(); } + adopt(parent: OutlineModel): OutlineGroup { + let res = new OutlineGroup(this.id, parent, this.provider, this.providerIndex); + forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res)); + return res; + } + updateMatches(pattern: string, topMatch: OutlineElement): OutlineElement { for (const key in this.children) { topMatch = this._updateMatches(pattern, this.children[key], topMatch); @@ -286,34 +301,7 @@ export class OutlineModel extends TreeElement { }); }); - return Promise.all(promises).then(() => { - - let count = 0; - for (const key in result._groups) { - let group = result._groups[key]; - if (first(group.children) === undefined) { // empty - delete result._groups[key]; - } else { - count += 1; - } - } - - if (count !== 1) { - // - result.children = result._groups; - - } else { - // adopt all elements of the first group - let group = first(result._groups); - for (let key in group.children) { - let child = group.children[key]; - child.parent = result; - result.children[child.id] = child; - } - } - - return result; - }); + return Promise.all(promises).then(() => result._compact()); } private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void { @@ -347,11 +335,38 @@ export class OutlineModel extends TreeElement { super(); } - dispose(): void { - + adopt(): OutlineModel { + let res = new OutlineModel(this.textModel); + forEach(this._groups, entry => res._groups[entry.key] = entry.value.adopt(res)); + return res._compact(); } - adopt(other: OutlineModel): boolean { + private _compact(): this { + let count = 0; + for (const key in this._groups) { + let group = this._groups[key]; + if (first(group.children) === undefined) { // empty + delete this._groups[key]; + } else { + count += 1; + } + } + if (count !== 1) { + // + this.children = this._groups; + } else { + // adopt all elements of the first group + let group = first(this._groups); + for (let key in group.children) { + let child = group.children[key]; + child.parent = this; + this.children[child.id] = child; + } + } + return this; + } + + merge(other: OutlineModel): boolean { if (this.textModel.uri.toString() !== other.textModel.uri.toString()) { return false; } diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 19489cbae98..72f66dd91e2 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, @@ -161,7 +162,7 @@ export class OutlineRenderer implements IRenderer { renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void { if (element instanceof OutlineElement) { - template.icon.className = `outline-element-icon symbol-icon ${symbolKindToCssClass(element.symbol.kind)}`; + template.icon.className = `outline-element-icon ${symbolKindToCssClass(element.symbol.kind)}`; template.label.set(element.symbol.name, element.score ? createMatches(element.score[1]) : undefined, localize('title.template', "{0} ({1})", element.symbol.name, OutlineRenderer._symbolKindNames[element.symbol.kind])); template.detail.innerText = element.symbol.detail || ''; this._renderMarkerInfo(element, template); diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 389901f50ff..43a305a49bc 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -495,7 +495,7 @@ class UnfoldAction extends FoldingAction { name: 'Unfold editor argument', description: `Property-value pairs that can be passed through this argument: * 'levels': Number of levels to unfold. If not set, defaults to 1. - * 'direction': If 'up', unfold given number of levels up otherwise unfolds down + * 'direction': If 'up', unfold given number of levels up otherwise unfolds down. * 'selectionLines': The start lines (0-based) of the editor selections to apply the unfold action to. If not set, the active selection(s) will be used. `, constraint: foldingArgumentsConstraint @@ -557,8 +557,8 @@ class FoldAction extends FoldingAction { { name: 'Fold editor argument', description: `Property-value pairs that can be passed through this argument: - * 'levels': Number of levels to fold. Defaults to 1 - * 'direction': If 'up', folds given number of levels up otherwise folds down + * 'levels': Number of levels to fold. Defaults to 1. + * 'direction': If 'up', folds given number of levels up otherwise folds down. * 'selectionLines': The start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used. `, constraint: foldingArgumentsConstraint diff --git a/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts b/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts index f8157b3bf0c..8bc694f358a 100644 --- a/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts +++ b/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts @@ -52,19 +52,20 @@ export class ClickLinkKeyboardEvent { this.hasTriggerModifier = hasModifier(source, opts.triggerModifier); } } +export type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'; export class ClickLinkOptions { public readonly triggerKey: KeyCode; - public readonly triggerModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'; + public readonly triggerModifier: TriggerModifier; public readonly triggerSideBySideKey: KeyCode; - public readonly triggerSideBySideModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'; + public readonly triggerSideBySideModifier: TriggerModifier; constructor( triggerKey: KeyCode, - triggerModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey', + triggerModifier: TriggerModifier, triggerSideBySideKey: KeyCode, - triggerSideBySideModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey' + triggerSideBySideModifier: TriggerModifier ) { this.triggerKey = triggerKey; this.triggerModifier = triggerModifier; diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinition.ts b/src/vs/editor/contrib/goToDefinition/goToDefinition.ts index eee248b2cb0..b0474016f0a 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinition.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinition.ts @@ -3,24 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - +import { flatten, coalesce } from 'vs/base/common/arrays'; +import { asWinJsPromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; 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 LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; -import { DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry, Location, DefinitionLink } from 'vs/editor/common/modes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { asWinJsPromise } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; -import { flatten } from 'vs/base/common/arrays'; +import { ITextModel } from 'vs/editor/common/model'; +import { DefinitionLink, DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry } from 'vs/editor/common/modes'; +import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; function getDefinitions( model: ITextModel, position: Position, registry: LanguageFeatureRegistry, - provide: (provider: T, model: ITextModel, position: Position, token: CancellationToken) => Location | Location[] | Thenable + provide: (provider: T, model: ITextModel, position: Position, token: CancellationToken) => DefinitionLink | DefinitionLink[] | Thenable ): TPromise { const provider = registry.ordered(model); @@ -35,7 +33,7 @@ function getDefinitions( }); return TPromise.join(promises) .then(flatten) - .then(references => references.filter(x => !!x)); + .then(references => coalesce(references)); } @@ -45,13 +43,13 @@ export function getDefinitionsAtPosition(model: ITextModel, position: Position): }); } -export function getImplementationsAtPosition(model: ITextModel, position: Position): TPromise { +export function getImplementationsAtPosition(model: ITextModel, position: Position): TPromise { return getDefinitions(model, position, ImplementationProviderRegistry, (provider, model, position, token) => { return provider.provideImplementation(model, position, token); }); } -export function getTypeDefinitionsAtPosition(model: ITextModel, position: Position): TPromise { +export function getTypeDefinitionsAtPosition(model: ITextModel, position: Position): TPromise { return getDefinitions(model, position, TypeDefinitionProviderRegistry, (provider, model, position, token) => { return provider.provideTypeDefinition(model, position, token); }); diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts index 8c15d98772c..981b9fc99dc 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as nls from 'vs/nls'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; @@ -13,7 +11,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { registerEditorAction, IActionOptions, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; -import { Location } from 'vs/editor/common/modes'; +import { DefinitionLink } from 'vs/editor/common/modes'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from './goToDefinition'; import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController'; import { ReferencesModel } from 'vs/editor/contrib/referenceSearch/referencesModel'; @@ -67,7 +65,7 @@ export class DefinitionAction extends EditorAction { // * remove falsy references // * find reference at the current pos let idxOfCurrent = -1; - let result: Location[] = []; + const result: DefinitionLink[] = []; for (let i = 0; i < references.length; i++) { let reference = references[i]; if (!reference || !reference.range) { @@ -112,7 +110,7 @@ export class DefinitionAction extends EditorAction { return definitionPromise; } - protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { + protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { return getDefinitionsAtPosition(model, position); } @@ -145,8 +143,8 @@ export class DefinitionAction extends EditorAction { } } - private _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: Location, sideBySide: boolean): TPromise { - let { uri, range } = reference; + private _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: DefinitionLink, sideBySide: boolean): TPromise { + const { uri, range } = reference; return editorService.openCodeEditor({ resource: uri, options: { @@ -247,7 +245,7 @@ export class PeekDefinitionAction extends DefinitionAction { } export class ImplementationAction extends DefinitionAction { - protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { + protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { return getImplementationsAtPosition(model, position); } @@ -303,7 +301,7 @@ export class PeekImplementationAction extends ImplementationAction { } export class TypeDefinitionAction extends DefinitionAction { - protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { + protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { return getTypeDefinitionsAtPosition(model, position); } diff --git a/src/vs/editor/contrib/hover/getHover.ts b/src/vs/editor/contrib/hover/getHover.ts index 466f04cab88..577c87d814a 100644 --- a/src/vs/editor/contrib/hover/getHover.ts +++ b/src/vs/editor/contrib/hover/getHover.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { coalesce } from 'vs/base/common/arrays'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { ITextModel } from 'vs/editor/common/model'; @@ -13,26 +11,26 @@ import { Hover, HoverProviderRegistry } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import { CancellationToken } from 'vs/base/common/cancellation'; -export function getHover(model: ITextModel, position: Position, token: CancellationToken = CancellationToken.None): Promise { +export function getHover(model: ITextModel, position: Position, token: CancellationToken): Promise { const supports = HoverProviderRegistry.ordered(model); - const values: Hover[] = []; - const promises = supports.map((support, idx) => { - 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; - if (hasRange && hasHtmlContent) { - values[idx] = result; - } - } + const promises = supports.map(support => { + return Promise.resolve(support.provideHover(model, position, token)).then(hover => { + return hover && isValid(hover) ? hover : undefined; }, err => { onUnexpectedExternalError(err); + return undefined; }); }); - return Promise.all(promises).then(() => coalesce(values)); + return Promise.all(promises).then(values => coalesce(values)); } -registerDefaultLanguageCommand('_executeHoverProvider', getHover); +registerDefaultLanguageCommand('_executeHoverProvider', (model, position) => getHover(model, position, CancellationToken.None)); + +function isValid(result: Hover) { + const hasRange = (typeof result.range !== 'undefined'); + const hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0; + return hasRange && hasHtmlContent; +} diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 3d695a84c97..9a5cfbdb383 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -475,26 +475,29 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { next(): boolean { const length = this.hints.signatures.length; + const last = (this.currentSignature % length) === (length - 1); - if (length < 2) { + // If there is only one signature, or we're on last signature of list + if (length < 2 || last) { this.cancel(); return false; } - this.currentSignature = (this.currentSignature + 1) % length; + this.currentSignature++; this.render(); return true; } previous(): boolean { const length = this.hints.signatures.length; + const first = this.currentSignature === 0; - if (length < 2) { + if (length < 2 || first) { this.cancel(); return false; } - this.currentSignature = (this.currentSignature - 1 + length) % length; + this.currentSignature--; this.render(); return true; } diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index 6a6f30d27d5..4136612df1f 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -17,7 +17,7 @@ export const Context = { MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken = CancellationToken.None): Promise { +export function provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): Promise { const supports = SignatureHelpProviderRegistry.ordered(model); @@ -26,4 +26,4 @@ export function provideSignatureHelp(model: ITextModel, position: Position, toke })); } -registerDefaultLanguageCommand('_executeSignatureHelpProvider', provideSignatureHelp); +registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) => provideSignatureHelp(model, position, CancellationToken.None)); diff --git a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts index cb21013f62b..ff8515fdd1e 100644 --- a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts @@ -138,7 +138,7 @@ let showReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resour return TPromise.as(controller.toggleWidget( new Range(position.lineNumber, position.column, position.lineNumber, position.column), - createCancelablePromise(_ => Promise.reject(new ReferencesModel(references))), + createCancelablePromise(_ => Promise.resolve(new ReferencesModel(references))), defaultReferenceSearchOptions)).then(() => true); }); }; @@ -294,4 +294,4 @@ export function provideReferences(model: ITextModel, position: Position, token: }); } -registerDefaultLanguageCommand('_executeReferenceProvider', provideReferences); +registerDefaultLanguageCommand('_executeReferenceProvider', (model, position) => provideReferences(model, position, CancellationToken.None)); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 7ef4daba9a4..d83828e5695 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -183,9 +183,6 @@ class RenameController implements IEditorContribution { } return this._bulkEditService.apply(result, { editor: this.editor }).then(result => { - if (result.selection) { - this.editor.setSelection(result.selection); - } // alert if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, newNameOrFocusFlag, result.ariaSummary)); 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 9bfe478497b..0df5abe1e5e 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -236,10 +236,16 @@ interface SuggestController extends IEditorContribution { triggerSuggest(onlyFrom?: ISuggestSupport[]): void; } -let _suggestions: ISuggestion[]; + let _provider = new class implements ISuggestSupport { + + onlyOnceSuggestions: ISuggestion[] = []; + provideCompletionItems(): ISuggestResult { - return _suggestions && { suggestions: _suggestions }; + let suggestions = this.onlyOnceSuggestions.slice(0); + let result = { suggestions }; + this.onlyOnceSuggestions.length = 0; + return result; } }; @@ -247,8 +253,7 @@ SuggestRegistry.register('*', _provider); export function showSimpleSuggestions(editor: ICodeEditor, suggestions: ISuggestion[]) { setTimeout(() => { - _suggestions = suggestions; + _provider.onlyOnceSuggestions.push(...suggestions); editor.getContribution('editor.contrib.suggestController').triggerSuggest([_provider]); - _suggestions = undefined; }, 0); } diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index aefe330316b..4a427b85dfe 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -18,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; @@ -273,6 +274,11 @@ export class SuggestModel implements IDisposable { 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(() => { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 6e72075c97e..3360b6fbda0 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -14,7 +14,7 @@ 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'; -import { IDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -173,7 +173,10 @@ class Renderer implements IRenderer { data.readMore.onmousedown = null; data.readMore.onclick = null; } + } + disposeElement(): void { + // noop } disposeTemplate(templateData: ISuggestionTemplateData): void { @@ -349,7 +352,7 @@ export interface ISelectedSuggestion { model: CompletionModel; } -export class SuggestWidget implements IContentWidget, IDelegate, IDisposable { +export class SuggestWidget implements IContentWidget, IVirtualDelegate, IDisposable { private static readonly ID: string = 'editor.widget.suggestWidget'; @@ -601,7 +604,11 @@ export class SuggestWidget implements IContentWidget, IDelegate } else { removeClass(this.element, 'docs-side'); } - }).catch(onUnexpectedError).then(() => this.currentSuggestionDetails = null); + }).catch(onUnexpectedError).then(() => { + if (this.focusedItem === item) { + this.currentSuggestionDetails = null; + } + }); // emit an event this.onDidFocusEmitter.fire({ item, index, model: this.completionModel }); @@ -878,7 +885,7 @@ export class SuggestWidget implements IContentWidget, IDelegate */ this.telemetryService.publicLog('suggestWidget:collapseDetails', this.editor.getTelemetryData()); } else { - if (this.state !== State.Open && this.state !== State.Details) { + if (this.state !== State.Open && this.state !== State.Details && this.state !== State.Frozen) { return; } 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 1b7b9016979..7f21bc957a1 100644 --- a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts @@ -36,7 +36,7 @@ export const overviewRulerWordHighlightStrongForeground = registerColor('editorO export const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); -export function getOccurrencesAtPosition(model: ITextModel, position: Position, token: CancellationToken = CancellationToken.None): Promise { +export function getOccurrencesAtPosition(model: ITextModel, position: Position, token: CancellationToken): Promise { const orderedByScore = DocumentHighlightProviderRegistry.ordered(model); @@ -49,7 +49,7 @@ export function getOccurrencesAtPosition(model: ITextModel, position: Position, }), result => !isFalsyOrEmpty(result)); } -registerDefaultLanguageCommand('_executeDocumentHighlights', getOccurrencesAtPosition); +registerDefaultLanguageCommand('_executeDocumentHighlights', (model, position) => getOccurrencesAtPosition(model, position, CancellationToken.None)); class WordHighlighter { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 08f50a00e19..d20f7346b83 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -8,7 +8,7 @@ import Severity from 'vs/base/common/severity'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IConfigurationService, IConfigurationChangeEvent, IConfigurationOverrides, IConfigurationData } from 'vs/platform/configuration/common/configuration'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ICommandService, ICommand, ICommandEvent, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IDisposable, IReference, ImmortalReference, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, IReference, ImmortalReference, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeybindingsRegistry, IKeybindingItem } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -230,11 +230,9 @@ export class StandaloneCommandService implements ICommandService { public addCommand(command: ICommand): IDisposable { const { id } = command; this._dynamicCommands[id] = command; - return { - dispose: () => { - delete this._dynamicCommands[id]; - } - }; + return toDisposable(() => { + delete this._dynamicCommands[id]; + }); } public executeCommand(id: string, ...args: any[]): TPromise { @@ -289,18 +287,16 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { weight2: 0 }); - toDispose.push({ - dispose: () => { - for (let i = 0; i < this._dynamicKeybindings.length; i++) { - let kb = this._dynamicKeybindings[i]; - if (kb.command === commandId) { - this._dynamicKeybindings.splice(i, 1); - this.updateResolver({ source: KeybindingSource.Default }); - return; - } + toDispose.push(toDisposable(() => { + for (let i = 0; i < this._dynamicKeybindings.length; i++) { + let kb = this._dynamicKeybindings[i]; + if (kb.command === commandId) { + this._dynamicKeybindings.splice(i, 1); + this.updateResolver({ source: KeybindingSource.Default }); + return; } } - }); + })); let commandService = this._commandService; if (commandService instanceof StandaloneCommandService) { diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 3a1fabd9093..6018f9f47d5 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -5,7 +5,7 @@ 'use strict'; -import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, combinedDisposable, toDisposable } 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'; @@ -17,7 +17,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { StandaloneKeybindingService, applyConfigurationValues } from 'vs/editor/standalone/browser/simpleServices'; -import { IEditorContextViewService } from 'vs/editor/standalone/browser/standaloneServices'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -272,11 +272,9 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon // Store it under the original id, such that trigger with the original id will work this._actions[id] = internalAction; - toDispose.push({ - dispose: () => { - delete this._actions[id]; - } - }); + toDispose.push(toDisposable(() => { + delete this._actions[id]; + })); return combinedDisposable(toDispose); } @@ -284,7 +282,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon export class StandaloneEditor extends StandaloneCodeEditor implements IStandaloneCodeEditor { - private _contextViewService: IEditorContextViewService; + private _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; private _ownsModel: boolean; @@ -311,7 +309,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon delete options.model; super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService, notificationService); - this._contextViewService = contextViewService; + this._contextViewService = contextViewService; this._configurationService = configurationService; this._register(toDispose); @@ -359,7 +357,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon export class StandaloneDiffEditor extends DiffEditorWidget implements IStandaloneDiffEditor { - private _contextViewService: IEditorContextViewService; + private _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; constructor( @@ -384,7 +382,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super(domElement, options, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService); - this._contextViewService = contextViewService; + this._contextViewService = contextViewService; this._configurationService = configurationService; this._register(toDispose); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index e4f58e5df58..d3275d83f30 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -45,11 +45,6 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -export interface IEditorContextViewService extends IContextViewService { - dispose(): void; - setContainer(domNode: HTMLElement): void; -} - export interface IEditorOverrideServices { [index: string]: any; } diff --git a/src/vs/loader.js b/src/vs/loader.js index 291f0e8b6b3..d2b9f26ab38 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -212,7 +212,7 @@ var AMDLoader; return '===anonymous' + (Utilities.NEXT_ANONYMOUS_ID++) + '==='; }; Utilities.isAnonymousModule = function (id) { - return /^===anonymous/.test(id); + return Utilities.startsWith(id, '===anonymous'); }; Utilities.getHighPerformanceTimestamp = function () { if (!this.PERFORMANCE_NOW_PROBED) { @@ -811,15 +811,17 @@ var AMDLoader; errorCode: 'cachedDataRejected', path: cachedDataPath }); - NodeScriptLoader._runSoon(function () { return _this._fs.unlink(cachedDataPath, function (err) { - if (err) { - moduleManager.getConfig().getOptionsLiteral().onNodeCachedData({ - errorCode: 'unlink', - path: cachedDataPath, - detail: err - }); - } - }); }, moduleManager.getConfig().getOptionsLiteral().nodeCachedDataWriteDelay); + NodeScriptLoader._runSoon(function () { + return _this._fs.unlink(cachedDataPath, function (err) { + if (err) { + moduleManager.getConfig().getOptionsLiteral().onNodeCachedData({ + errorCode: 'unlink', + path: cachedDataPath, + detail: err + }); + } + }); + }, moduleManager.getConfig().getOptionsLiteral().nodeCachedDataWriteDelay); } else if (script.cachedDataProduced) { // data produced => tell outside world @@ -828,15 +830,17 @@ var AMDLoader; length: script.cachedData.length }); // data produced => write cache file - NodeScriptLoader._runSoon(function () { return _this._fs.writeFile(cachedDataPath, script.cachedData, function (err) { - if (err) { - moduleManager.getConfig().getOptionsLiteral().onNodeCachedData({ - errorCode: 'writeFile', - path: cachedDataPath, - detail: err - }); - } - }); }, moduleManager.getConfig().getOptionsLiteral().nodeCachedDataWriteDelay); + NodeScriptLoader._runSoon(function () { + return _this._fs.writeFile(cachedDataPath, script.cachedData, function (err) { + if (err) { + moduleManager.getConfig().getOptionsLiteral().onNodeCachedData({ + errorCode: 'writeFile', + path: cachedDataPath, + detail: err + }); + } + }); + }, moduleManager.getConfig().getOptionsLiteral().nodeCachedDataWriteDelay); } }; NodeScriptLoader._runSoon = function (callback, minTimeout) { @@ -1403,7 +1407,8 @@ var AMDLoader; this._knownModules2[moduleId] = true; var strModuleId = this._moduleIdProvider.getStrModuleId(moduleId); var paths = this._config.moduleIdToPaths(strModuleId); - if (this._env.isNode && strModuleId.indexOf('/') === -1) { + var scopedPackageRegex = /^@[^\/]+\/[^\/]+$/; // matches @scope/package-name + if (this._env.isNode && (strModuleId.indexOf('/') === -1 || scopedPackageRegex.test(strModuleId))) { paths.push('node|' + strModuleId); } var lastPathIndex = -1; @@ -1649,6 +1654,9 @@ var AMDLoader; RequireFunc.getStats = function () { return moduleManager.getLoaderEvents(); }; + RequireFunc.define = function () { + return DefineFunc.apply(null, arguments); + }; function init() { if (typeof AMDLoader.global.require !== 'undefined' || typeof require !== 'undefined') { var _nodeRequire_1 = (AMDLoader.global.require || require); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2a2ea6db909..438105b4d89 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2513,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; } /** @@ -3123,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 { @@ -3865,9 +3870,9 @@ declare namespace monaco.editor { * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. * @param source The source of the call. * @param edits The edits to execute. - * @param endCursoState Cursor state after the edits were applied. + * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursoState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; /** * Execute multiple (concommitent) commands on the editor. * @param source The source of the call. @@ -4917,7 +4922,7 @@ declare namespace monaco.languages { /** * Provide the definition of the symbol at the given position and document. */ - provideDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): DefinitionLink | Thenable; + provideDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | DefinitionLink[] | Thenable; } /** @@ -4928,7 +4933,7 @@ declare namespace monaco.languages { /** * Provide the implementation of the symbol at the given position and document. */ - provideImplementation(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideImplementation(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | DefinitionLink[] | Thenable; } /** @@ -4939,7 +4944,7 @@ declare namespace monaco.languages { /** * Provide the type definition of the symbol at the given position and document. */ - provideTypeDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideTypeDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | DefinitionLink[] | Thenable; } /** @@ -5205,6 +5210,7 @@ declare namespace monaco.languages { newUri: Uri; options: { overwrite?: boolean; + ignoreIfNotExists?: boolean; ignoreIfExists?: boolean; recursive?: boolean; }; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c4b1e0f958c..e919252f47a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -90,6 +90,7 @@ export class MenuId { static readonly MenubarRecentMenu = new MenuId(); static readonly MenubarSelectionMenu = new MenuId(); static readonly MenubarViewMenu = new MenuId(); + static readonly MenubarAppearanceMenu = new MenuId(); static readonly MenubarLayoutMenu = new MenuId(); static readonly MenubarGoMenu = new MenuId(); static readonly MenubarDebugMenu = new MenuId(); diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index 1d77135f933..4d2595d83ef 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -5,11 +5,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import URI from 'vs/base/common/uri'; export interface IBackupWorkspacesFormat { rootWorkspaces: IWorkspaceIdentifier[]; - folderWorkspaces: string[]; + folderURIWorkspaces: string[]; emptyWorkspaces: string[]; + + // deprecated + folderWorkspaces?: string[]; // use folderURIWorkspaces instead } export const IBackupMainService = createDecorator('backupMainService'); @@ -20,10 +24,14 @@ export interface IBackupMainService { isHotExitEnabled(): boolean; getWorkspaceBackups(): IWorkspaceIdentifier[]; - getFolderBackupPaths(): string[]; + getFolderBackupPaths(): URI[]; getEmptyWindowBackupPaths(): string[]; registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string; - registerFolderBackupSync(folderPath: string): string; + registerFolderBackupSync(folderUri: URI): string; registerEmptyWindowBackupSync(backupFolder?: string): string; + + unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void; + unregisterFolderBackupSync(folderUri: URI): void; + unregisterEmptyWindowBackupSync(backupFolder: string): void; } \ No newline at end of file diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 9201f86adb9..8c879b66d8b 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -3,18 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as arrays from 'vs/base/common/arrays'; import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import * as platform from 'vs/base/common/platform'; import * as extfs from 'vs/base/node/extfs'; -import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup'; +import * as arrays from 'vs/base/common/arrays'; +import { IBackupMainService, IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import URI from 'vs/base/common/uri'; +import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources'; +import { isEqual } from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; export class BackupMainService implements IBackupMainService { @@ -23,7 +28,9 @@ export class BackupMainService implements IBackupMainService { protected backupHome: string; protected workspacesJsonPath: string; - protected backups: IBackupWorkspacesFormat; + protected rootWorkspaces: IWorkspaceIdentifier[]; + protected folderWorkspaces: URI[]; + protected emptyWorkspaces: string[]; constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -43,17 +50,16 @@ export class BackupMainService implements IBackupMainService { return []; } - return this.backups.rootWorkspaces.slice(0); // return a copy + return this.rootWorkspaces.slice(0); // return a copy } - public getFolderBackupPaths(): string[] { + public getFolderBackupPaths(): URI[] { if (this.isHotExitOnExitAndWindowClose()) { // Only non-folder windows are restored on main process launch when // hot exit is configured as onExitAndWindowClose. return []; } - - return this.backups.folderWorkspaces.slice(0); // return a copy + return this.folderWorkspaces.slice(0); // return a copy } public isHotExitEnabled(): boolean { @@ -71,13 +77,16 @@ export class BackupMainService implements IBackupMainService { } public getEmptyWindowBackupPaths(): string[] { - return this.backups.emptyWorkspaces.slice(0); // return a copy + return this.emptyWorkspaces.slice(0); // return a copy } public registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string { - this.pushBackupPathsSync(workspace, this.backups.rootWorkspaces); + if (!this.rootWorkspaces.some(w => w.id === workspace.id)) { + this.rootWorkspaces.push(workspace); + this.saveSync(); + } - const backupPath = path.join(this.backupHome, workspace.id); + const backupPath = this.getBackupPath(workspace.id); if (migrateFrom) { this.moveBackupFolderSync(backupPath, migrateFrom); @@ -103,10 +112,28 @@ export class BackupMainService implements IBackupMainService { } } - public registerFolderBackupSync(folderPath: string): string { - this.pushBackupPathsSync(folderPath, this.backups.folderWorkspaces); + public unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void { + let index = arrays.firstIndex(this.rootWorkspaces, w => w.id === workspace.id); + if (index !== -1) { + this.rootWorkspaces.splice(index, 1); + this.saveSync(); + } + } - return path.join(this.backupHome, this.getFolderHash(folderPath)); + public registerFolderBackupSync(folderUri: URI): string { + if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri, hasToIgnoreCase(folderUri)))) { + this.folderWorkspaces.push(folderUri); + this.saveSync(); + } + return this.getBackupPath(this.getFolderHash(folderUri)); + } + + public unregisterFolderBackupSync(folderUri: URI): void { + let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri, hasToIgnoreCase(folderUri))); + if (index !== -1) { + this.folderWorkspaces.splice(index, 1); + this.saveSync(); + } } public registerEmptyWindowBackupSync(backupFolder?: string): string { @@ -115,52 +142,23 @@ export class BackupMainService implements IBackupMainService { if (!backupFolder) { backupFolder = this.getRandomEmptyWindowId(); } - - this.pushBackupPathsSync(backupFolder, this.backups.emptyWorkspaces); - - return path.join(this.backupHome, backupFolder); + if (!this.emptyWorkspaces.some(w => isEqual(w, backupFolder, !platform.isLinux))) { + this.emptyWorkspaces.push(backupFolder); + this.saveSync(); + } + return this.getBackupPath(backupFolder); } - private pushBackupPathsSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void { - if (this.indexOf(workspaceIdentifier, target) === -1) { - target.push(workspaceIdentifier); + public unregisterEmptyWindowBackupSync(backupFolder: string): void { + let index = arrays.firstIndex(this.emptyWorkspaces, w => isEqual(w, backupFolder, !platform.isLinux)); + if (index !== -1) { + this.emptyWorkspaces.splice(index, 1); this.saveSync(); } } - protected removeBackupPathSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void { - if (!target) { - return; - } - - const index = this.indexOf(workspaceIdentifier, target); - if (index === -1) { - return; - } - - target.splice(index, 1); - this.saveSync(); - } - - private indexOf(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): number { - if (!target) { - return -1; - } - - const sanitizedWorkspaceIdentifier = this.sanitizeId(workspaceIdentifier); - - return arrays.firstIndex(target, id => this.sanitizeId(id) === sanitizedWorkspaceIdentifier); - } - - private sanitizeId(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { - if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return this.sanitizePath(workspaceIdentifier); - } - - return workspaceIdentifier.id; - } - protected loadSync(): void { + let backups: IBackupWorkspacesFormat; try { backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here @@ -168,117 +166,168 @@ export class BackupMainService implements IBackupMainService { backups = Object.create(null); } - // Ensure rootWorkspaces is a object[] - if (backups.rootWorkspaces) { - const rws = backups.rootWorkspaces; - if (!Array.isArray(rws) || rws.some(r => typeof r !== 'object')) { - backups.rootWorkspaces = []; - } - } else { - backups.rootWorkspaces = []; - } + // read empty worrkspace backs first + this.emptyWorkspaces = this.validateEmptyWorkspaces(backups.emptyWorkspaces); - // Ensure folderWorkspaces is a string[] - if (backups.folderWorkspaces) { - const fws = backups.folderWorkspaces; - if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) { - backups.folderWorkspaces = []; - } - } else { - backups.folderWorkspaces = []; - } + // read workspace backups + this.rootWorkspaces = this.validateWorkspaces(backups.rootWorkspaces); - // Ensure emptyWorkspaces is a string[] - if (backups.emptyWorkspaces) { - const fws = backups.emptyWorkspaces; - if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) { - backups.emptyWorkspaces = []; - } - } else { - backups.emptyWorkspaces = []; - } - - this.backups = this.dedupeBackups(backups); - - // Validate backup workspaces - this.validateBackupWorkspaces(backups); - } - - protected dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { - - // De-duplicate folder/workspace backups. don't worry about cleaning them up any duplicates as - // they will be removed when there are no backups. - backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces, ws => this.sanitizePath(ws)); - backups.rootWorkspaces = arrays.distinct(backups.rootWorkspaces, ws => this.sanitizePath(ws.id)); - - return backups; - } - - private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void { - const staleBackupWorkspaces: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; backupPath: string; target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = []; - - const workspaceAndFolders: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = []; - workspaceAndFolders.push(...backups.rootWorkspaces.map(r => ({ workspaceIdentifier: r, target: backups.rootWorkspaces }))); - workspaceAndFolders.push(...backups.folderWorkspaces.map(f => ({ workspaceIdentifier: f, target: backups.folderWorkspaces }))); - - // Validate Workspace and Folder Backups - workspaceAndFolders.forEach(workspaceOrFolder => { - const workspaceId = workspaceOrFolder.workspaceIdentifier; - const workspacePath = isSingleFolderWorkspaceIdentifier(workspaceId) ? workspaceId : workspaceId.configPath; - const backupPath = path.join(this.backupHome, isSingleFolderWorkspaceIdentifier(workspaceId) ? this.getFolderHash(workspaceId) : workspaceId.id); - const hasBackups = this.hasBackupsSync(backupPath); - const missingWorkspace = hasBackups && !fs.existsSync(workspacePath); - - // If the workspace/folder has no backups, make sure to delete it - // If the workspace/folder has backups, but the target workspace is missing, convert backups to empty ones - if (!hasBackups || missingWorkspace) { - staleBackupWorkspaces.push({ workspaceIdentifier: workspaceId, backupPath, target: workspaceOrFolder.target }); - - if (missingWorkspace) { - this.convertToEmptyWindowBackup(backupPath); + // read folder backups + let workspaceFolders: URI[]; + try { + if (Array.isArray(backups.folderURIWorkspaces)) { + workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f)); + } else if (Array.isArray(backups.folderWorkspaces)) { + // migrate legacy folder paths + workspaceFolders = []; + for (const folderPath of backups.folderWorkspaces) { + const oldFolderHash = this.getLegacyFolderHash(folderPath); + const folderUri = URI.file(folderPath); + const newFolderHash = this.getFolderHash(folderUri); + if (newFolderHash !== oldFolderHash) { + this.moveBackupFolderSync(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash)); + } + workspaceFolders.push(folderUri); } } - }); + } catch (e) { + // ignore URI parsing exceptions + } + this.folderWorkspaces = this.validateFolders(workspaceFolders); + + // save again in case some workspaces or folders have been removed + this.saveSync(); + + } + + private getBackupPath(oldFolderHash: string): string { + return path.join(this.backupHome, oldFolderHash); + } + + private validateWorkspaces(rootWorkspaces: IWorkspaceIdentifier[]): IWorkspaceIdentifier[] { + if (!Array.isArray(rootWorkspaces)) { + return []; + } + + const seenIds: { [id: string]: boolean } = Object.create(null); + const result: IWorkspaceIdentifier[] = []; + + // Validate Workspaces + for (let workspace of rootWorkspaces) { + if (!isWorkspaceIdentifier(workspace)) { + return []; // wrong format, skip all entries + } + + if (!seenIds[workspace.id]) { + seenIds[workspace.id] = true; + + const backupPath = this.getBackupPath(workspace.id); + const hasBackups = this.hasBackupsSync(backupPath); + + // If the workspace has no backups, ignore it + if (hasBackups) { + if (fs.existsSync(workspace.configPath)) { + result.push(workspace); + } else { + // If the workspace has backups, but the target workspace is missing, convert backups to empty ones + this.convertToEmptyWindowBackup(backupPath); + } + } else { + this.deleteStaleBackup(backupPath); + } + } + } + return result; + } + + private validateFolders(folderWorkspaces: URI[]): URI[] { + if (!Array.isArray(folderWorkspaces)) { + return []; + } + + const result: URI[] = []; + const seen: { [id: string]: boolean } = Object.create(null); + + for (let folderURI of folderWorkspaces) { + const key = getComparisonKey(folderURI); + if (!seen[key]) { + seen[key] = true; + + const backupPath = this.getBackupPath(this.getFolderHash(folderURI)); + const hasBackups = this.hasBackupsSync(backupPath); + + // If the folder has no backups, ignore it + if (hasBackups) { + if (folderURI.scheme !== Schemas.file || fs.existsSync(folderURI.fsPath)) { + result.push(folderURI); + } else { + // If the folder has backups, but the target workspace is missing, convert backups to empty ones + this.convertToEmptyWindowBackup(backupPath); + } + } else { + this.deleteStaleBackup(backupPath); + } + } + } + + return result; + } + private validateEmptyWorkspaces(emptyWorkspaces: string[]): string[] { + if (!Array.isArray(emptyWorkspaces)) { + return []; + } + + const result: string[] = []; + const seen: { [id: string]: boolean } = Object.create(null); // Validate Empty Windows - backups.emptyWorkspaces.forEach(backupFolder => { - const backupPath = path.join(this.backupHome, backupFolder); - if (!this.hasBackupsSync(backupPath)) { - staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, target: backups.emptyWorkspaces }); + for (let backupFolder of emptyWorkspaces) { + if (typeof backupFolder !== 'string') { + return []; } - }); - // Clean up stale backups - staleBackupWorkspaces.forEach(staleBackupWorkspace => { - const { backupPath, workspaceIdentifier, target } = staleBackupWorkspace; + if (!seen[backupFolder]) { + seen[backupFolder] = true; - try { + const backupPath = this.getBackupPath(backupFolder); + if (this.hasBackupsSync(backupPath)) { + result.push(backupFolder); + } else { + this.deleteStaleBackup(backupPath); + } + } + } + + return result; + } + + private deleteStaleBackup(backupPath: string) { + try { + if (fs.existsSync(backupPath)) { extfs.delSync(backupPath); - } catch (ex) { - this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); } - - this.removeBackupPathSync(workspaceIdentifier, target); - }); + } catch (ex) { + this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); + } } private convertToEmptyWindowBackup(backupPath: string): boolean { // New empty window backup - const identifier = this.getRandomEmptyWindowId(); - this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces); + let newBackupFolder = this.getRandomEmptyWindowId(); + while (this.emptyWorkspaces.some(w => isEqual(w, newBackupFolder, platform.isLinux))) { + newBackupFolder = this.getRandomEmptyWindowId(); + } // Rename backupPath to new empty window backup path - const newEmptyWindowBackupPath = path.join(this.backupHome, identifier); + const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { fs.renameSync(backupPath, newEmptyWindowBackupPath); } catch (ex) { this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); - - this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces); - return false; } + this.emptyWorkspaces.push(newBackupFolder); return true; } @@ -308,8 +357,12 @@ export class BackupMainService implements IBackupMainService { if (!fs.existsSync(this.backupHome)) { fs.mkdirSync(this.backupHome); } - - extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(this.backups)); + const backups: IBackupWorkspacesFormat = { + rootWorkspaces: this.rootWorkspaces, + folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()), + emptyWorkspaces: this.emptyWorkspaces + }; + extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(backups)); } catch (ex) { this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); } @@ -319,11 +372,19 @@ export class BackupMainService implements IBackupMainService { return (Date.now() + Math.round(Math.random() * 1000)).toString(); } - private sanitizePath(p: string): string { - return platform.isLinux ? p : p.toLowerCase(); + protected getFolderHash(folderUri: URI): string { + let key; + if (folderUri.scheme === Schemas.file) { + // for backward compatibility, use the fspath as key + key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); + + } else { + key = hasToIgnoreCase(folderUri) ? folderUri.toString().toLowerCase() : folderUri.toString(); + } + return crypto.createHash('md5').update(key).digest('hex'); } - protected getFolderHash(folderPath: string): string { - return crypto.createHash('md5').update(this.sanitizePath(folderPath)).digest('hex'); + protected getLegacyFolderHash(folderPath: string): string { + return crypto.createHash('md5').update(platform.isLinux ? folderPath : folderPath.toLowerCase()).digest('hex'); } -} +} \ No newline at end of file diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index ff421ddbdf7..ab249bac7dc 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -25,6 +25,11 @@ import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; suite('BackupMainService', () => { + + function assertEqualUris(actual: Uri[], expected: Uri[]) { + assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); + } + const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice'); const backupHome = path.join(parentDir, 'Backups'); const backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); @@ -43,28 +48,21 @@ suite('BackupMainService', () => { this.loadSync(); } - public get backupsData(): IBackupWorkspacesFormat { - return this.backups; - } - - public removeBackupPathSync(workspaceIdentifier: string | IWorkspaceIdentifier, target: (string | IWorkspaceIdentifier)[]): void { - return super.removeBackupPathSync(workspaceIdentifier, target); - } - public loadSync(): void { super.loadSync(); } - public dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { - return super.dedupeBackups(backups); + public toBackupPath(arg: Uri | string): string { + const id = arg instanceof Uri ? super.getFolderHash(arg) : arg; + return path.join(this.backupHome, id); } - public toBackupPath(workspacePath: string): string { - return path.join(this.backupHome, super.getFolderHash(workspacePath)); + public getFolderHash(folderUri: Uri): string { + return super.getFolderHash(folderUri); } - public getFolderHash(folderPath: string): string { - return super.getFolderHash(folderPath); + public toLegacyBackupPath(folderPath: string): string { + return path.join(this.backupHome, super.getLegacyFolderHash(folderPath)); } } @@ -75,6 +73,31 @@ suite('BackupMainService', () => { }; } + async function ensureFolderExists(uri: Uri): Promise { + if (!fs.existsSync(uri.fsPath)) { + fs.mkdirSync(uri.fsPath); + } + const backupFolder = service.toBackupPath(uri); + await createBackupFolder(backupFolder); + } + + async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise { + if (!fs.existsSync(workspace.configPath)) { + await pfs.writeFile(workspace.configPath, 'Hello'); + } + const backupFolder = service.toBackupPath(workspace.id); + await createBackupFolder(backupFolder); + return workspace; + } + + async function createBackupFolder(backupFolder: string): Promise { + if (!fs.existsSync(backupFolder)) { + fs.mkdirSync(backupFolder); + fs.mkdirSync(path.join(backupFolder, Schemas.file)); + await pfs.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello'); + } + } + function sanitizePath(p: string): string { return platform.isLinux ? p : p.toLowerCase(); } @@ -82,6 +105,8 @@ suite('BackupMainService', () => { const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo'); const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar'); + const existingTestFolder1 = Uri.file(path.join(parentDir, 'folder1')); + let service: TestBackupMainService; let configService: TestConfigurationService; @@ -103,40 +128,40 @@ suite('BackupMainService', () => { this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); // 2) backup workspace path exists with empty contents within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assertEqualUris(service.getFolderBackupPaths(), []); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 3) backup workspace path exists with empty folders within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), Schemas.file)); - fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), Schemas.untitled)); - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); + fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file)); + fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled)); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assertEqualUris(service.getFolderBackupPaths(), []); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 4) backup workspace path points to a workspace that no longer exists // so it should convert the backup worspace to an empty workspace backup - const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), Schemas.file); - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); - service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(fooFile); assert.equal(service.getFolderBackupPaths().length, 1); assert.equal(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); @@ -155,32 +180,32 @@ suite('BackupMainService', () => { assert.deepEqual(service.getWorkspaceBackups(), []); // 2) backup workspace path exists with empty contents within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); service.loadSync(); assert.deepEqual(service.getWorkspaceBackups(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 3) backup workspace path exists with empty folders within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), Schemas.file)); - fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), Schemas.untitled)); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); + fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file)); + fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled)); service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); service.loadSync(); assert.deepEqual(service.getWorkspaceBackups(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 4) backup workspace path points to a workspace that no longer exists // so it should convert the backup worspace to an empty workspace backup - const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), Schemas.file); - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); assert.equal(service.getWorkspaceBackups().length, 1); @@ -192,10 +217,10 @@ suite('BackupMainService', () => { }); test('service supports to migrate backup data from another location', () => { - const backupPathToMigrate = service.toBackupPath(fooFile.fsPath); + const backupPathToMigrate = service.toBackupPath(fooFile); fs.mkdirSync(backupPathToMigrate); fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); - service.registerFolderBackupSync(backupPathToMigrate); + service.registerFolderBackupSync(Uri.file(backupPathToMigrate)); const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate); @@ -208,15 +233,15 @@ suite('BackupMainService', () => { }); test('service backup migration makes sure to preserve existing backups', () => { - const backupPathToMigrate = service.toBackupPath(fooFile.fsPath); + const backupPathToMigrate = service.toBackupPath(fooFile); fs.mkdirSync(backupPathToMigrate); fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); - service.registerFolderBackupSync(backupPathToMigrate); + service.registerFolderBackupSync(Uri.file(backupPathToMigrate)); - const backupPathToPreserve = service.toBackupPath(barFile.fsPath); + const backupPathToPreserve = service.toBackupPath(barFile); fs.mkdirSync(backupPathToPreserve); fs.writeFileSync(path.join(backupPathToPreserve, 'backup.txt'), 'Some Data'); - service.registerFolderBackupSync(backupPathToPreserve); + service.registerFolderBackupSync(Uri.file(backupPathToPreserve)); const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate); @@ -229,56 +254,102 @@ suite('BackupMainService', () => { assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0])).length); }); + suite('migrate folderPath to folderURI', () => { + + test('migration makes sure to preserve existing backups', async () => { + if (platform.isLinux) { + return; // TODO:Martin #54483 fix tests + } + + let path1 = path.join(parentDir, 'folder1').toLowerCase(); + let path2 = path.join(parentDir, 'folder2').toUpperCase(); + let uri1 = Uri.file(path1); + let uri2 = Uri.file(path2); + + if (!fs.existsSync(path1)) { + fs.mkdirSync(path1); + } + if (!fs.existsSync(path2)) { + fs.mkdirSync(path2); + } + const backupFolder1 = service.toLegacyBackupPath(path1); + if (!fs.existsSync(backupFolder1)) { + fs.mkdirSync(backupFolder1); + fs.mkdirSync(path.join(backupFolder1, Schemas.file)); + await pfs.writeFile(path.join(backupFolder1, Schemas.file, 'unsaved1.txt'), 'Legacy'); + } + const backupFolder2 = service.toLegacyBackupPath(path2); + if (!fs.existsSync(backupFolder2)) { + fs.mkdirSync(backupFolder2); + fs.mkdirSync(path.join(backupFolder2, Schemas.file)); + await pfs.writeFile(path.join(backupFolder2, Schemas.file, 'unsaved2.txt'), 'Legacy'); + } + + const workspacesJson = { rootWorkspaces: [], folderWorkspaces: [path1, path2], emptyWorkspaces: [] }; + await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { + const json = JSON.parse(content); + assert.deepEqual(json.folderURIWorkspaces, [uri1.toString(), uri2.toString()]); + const newBackupFolder1 = service.toBackupPath(uri1); + assert.ok(fs.existsSync(path.join(newBackupFolder1, Schemas.file, 'unsaved1.txt'))); + const newBackupFolder2 = service.toBackupPath(uri2); + assert.ok(fs.existsSync(path.join(newBackupFolder2, Schemas.file, 'unsaved2.txt'))); + }); + }); + }); + }); + suite('loadSync', () => { test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', () => { fs.writeFileSync(backupWorkspacesPath, ''); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', () => { fs.writeFileSync(backupWorkspacesPath, '{}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', () => { - service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); - assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]); + service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); + assertEqualUris(service.getFolderBackupPaths(), [Uri.file(fooFile.fsPath.toUpperCase())]); configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => { @@ -379,59 +450,82 @@ suite('BackupMainService', () => { }); suite('dedupeFolderWorkspaces', () => { - test('should ignore duplicates on Windows and Mac (folder workspace)', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } + test('should ignore duplicates (folder workspace)', async () => { - const backups: IBackupWorkspacesFormat = { + await ensureFolderExists(existingTestFolder1); + + const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], - folderWorkspaces: platform.isWindows ? ['c:\\FOO', 'C:\\FOO', 'c:\\foo'] : ['/FOO', '/foo'], + folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()], emptyWorkspaces: [] }; - - service.dedupeBackups(backups); - - assert.equal(backups.folderWorkspaces.length, 1); - if (platform.isWindows) { - assert.deepEqual(backups.folderWorkspaces, ['c:\\FOO'], 'should return the first duplicated entry'); - } else { - assert.deepEqual(backups.folderWorkspaces, ['/FOO'], 'should return the first duplicated entry'); - } + return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + }); + }); }); - test('should ignore duplicates on Windows and Mac (root workspace)', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } + test('should ignore duplicates on Windows and Mac (folder workspace)', async () => { - const backups: IBackupWorkspacesFormat = { - rootWorkspaces: platform.isWindows ? [toWorkspace('c:\\FOO'), toWorkspace('C:\\FOO'), toWorkspace('c:\\foo')] : [toWorkspace('/FOO'), toWorkspace('/foo')], - folderWorkspaces: [], + await ensureFolderExists(existingTestFolder1); + + const workspacesJson: IBackupWorkspacesFormat = { + rootWorkspaces: [], + folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()], emptyWorkspaces: [] }; + return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + }); + }); + }); - service.dedupeBackups(backups); - - assert.equal(backups.rootWorkspaces.length, 1); - if (platform.isWindows) { - assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['c:\\FOO'], 'should return the first duplicated entry'); - } else { - assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['/FOO'], 'should return the first duplicated entry'); + test('should ignore duplicates on Windows and Mac (root workspace)', async () => { + if (platform.isLinux) { + return; // TODO:Martin #54483 fix tests } + + const workspacePath = path.join(parentDir, 'Foo.code-workspace'); + + const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath)); + const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath.toUpperCase())); + const workspace3 = await ensureWorkspaceExists(toWorkspace(workspacePath.toLowerCase())); + + const workspacesJson: IBackupWorkspacesFormat = { + rootWorkspaces: [workspace1, workspace2, workspace3], + folderURIWorkspaces: [], + emptyWorkspaces: [] + }; + return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.equal(json.rootWorkspaces.length, platform.isLinux ? 3 : 1); + if (platform.isLinux) { + assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [workspacePath, workspacePath.toUpperCase(), workspacePath.toLowerCase()]); + } else { + assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [workspacePath], 'should return the first duplicated entry'); + } + }); + }); + }); }); suite('registerWindowForBackups', () => { test('should persist paths to workspaces.json (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); - assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath, barFile.fsPath]); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); + assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); - assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath, barFile.fsPath]); + assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); }); @@ -455,11 +549,11 @@ suite('BackupMainService', () => { }); test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); - assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]); + service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); + assertEqualUris(service.getFolderBackupPaths(), [Uri.file(fooFile.fsPath.toUpperCase())]); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); - assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath.toUpperCase()]); + assert.deepEqual(json.folderURIWorkspaces, [Uri.file(fooFile.fsPath.toUpperCase()).toString()]); }); }); @@ -475,16 +569,16 @@ suite('BackupMainService', () => { suite('removeBackupPathSync', () => { test('should remove folder workspaces from workspaces.json (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); + service.unregisterFolderBackupSync(fooFile); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); - assert.deepEqual(json.folderWorkspaces, [barFile.fsPath]); - service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces); + assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); + service.unregisterFolderBackupSync(barFile); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); - assert.deepEqual(json2.folderWorkspaces, []); + assert.deepEqual(json2.folderURIWorkspaces, []); }); }); }); @@ -494,11 +588,11 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws1); const ws2 = toWorkspace(barFile.fsPath); service.registerWorkspaceBackupSync(ws2); - service.removeBackupPathSync(ws1, service.backupsData.rootWorkspaces); + service.unregisterWorkspaceBackupSync(ws1); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [barFile.fsPath]); - service.removeBackupPathSync(ws2, service.backupsData.rootWorkspaces); + service.unregisterWorkspaceBackupSync(ws2); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); assert.deepEqual(json2.rootWorkspaces, []); @@ -509,11 +603,11 @@ suite('BackupMainService', () => { test('should remove empty workspaces from workspaces.json', () => { service.registerEmptyWindowBackupSync('foo'); service.registerEmptyWindowBackupSync('bar'); - service.removeBackupPathSync('foo', service.backupsData.emptyWorkspaces); + service.unregisterEmptyWindowBackupSync('foo'); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.emptyWorkspaces, ['bar']); - service.removeBackupPathSync('bar', service.backupsData.emptyWorkspaces); + service.unregisterEmptyWindowBackupSync('bar'); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); assert.deepEqual(json2.emptyWorkspaces, []); @@ -521,23 +615,24 @@ suite('BackupMainService', () => { }); }); - test('should fail gracefully when removing a path that doesn\'t exist', () => { - const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderWorkspaces: [fooFile.fsPath], emptyWorkspaces: [] }; + test('should fail gracefully when removing a path that doesn\'t exist', async () => { + + await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync + + const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaces: [] }; return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { - service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces); - service.removeBackupPathSync('test', service.backupsData.emptyWorkspaces); + service.loadSync(); + service.unregisterFolderBackupSync(barFile); + service.unregisterEmptyWindowBackupSync('test'); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json = JSON.parse(content); - assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath]); + assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); }); }); }); suite('getWorkspaceHash', () => { - test('should perform an md5 hash on the path', () => { - assert.equal(service.getFolderHash('/foo'), '1effb2475fcfba4f9e8b8a1dbc8f3caf'); - }); test('should ignore case on Windows and Mac', () => { // Skip test on Linux @@ -546,19 +641,19 @@ suite('BackupMainService', () => { } if (platform.isMacintosh) { - assert.equal(service.getFolderHash('/foo'), service.getFolderHash('/FOO')); + assert.equal(service.getFolderHash(Uri.file('/foo')), service.getFolderHash(Uri.file('/FOO'))); } if (platform.isWindows) { - assert.equal(service.getFolderHash('c:\\foo'), service.getFolderHash('C:\\FOO')); + assert.equal(service.getFolderHash(Uri.file('c:\\foo')), service.getFolderHash(Uri.file('C:\\FOO'))); } }); }); suite('mixed path casing', () => { test('should handle case insensitive paths properly (registerWindowForBackupsSync) (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { assert.equal(service.getFolderBackupPaths().length, 2); @@ -581,13 +676,13 @@ suite('BackupMainService', () => { test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', () => { // same case - service.registerFolderBackupSync(fooFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces); + service.registerFolderBackupSync(fooFile); + service.unregisterFolderBackupSync(fooFile); assert.equal(service.getFolderBackupPaths().length, 0); // mixed case - service.registerFolderBackupSync(fooFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath.toUpperCase(), service.backupsData.folderWorkspaces); + service.registerFolderBackupSync(fooFile); + service.unregisterFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { assert.equal(service.getFolderBackupPaths().length, 1); diff --git a/src/vs/platform/commands/common/commands.ts b/src/vs/platform/commands/common/commands.ts index 7d52c3ecaa0..b5f23e0b9f3 100644 --- a/src/vs/platform/commands/common/commands.ts +++ b/src/vs/platform/commands/common/commands.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TypeConstraint, validateConstraints } from 'vs/base/common/types'; import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; @@ -91,14 +91,12 @@ export const CommandsRegistry: ICommandRegistry = new class implements ICommandR let removeFn = commands.unshift(idOrCommand); - return { - dispose: () => { - removeFn(); - if (this._commands.get(id).isEmpty()) { - this._commands.delete(id); - } + return toDisposable(() => { + removeFn(); + if (this._commands.get(id).isEmpty()) { + this._commands.delete(id); } - }; + }); } getCommand(id: string): ICommand { diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index f0b8c1270c8..05e1e4439dd 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -9,7 +9,7 @@ import 'vs/css!./contextMenuHandler'; import { $, Builder } from 'vs/base/browser/builder'; import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IActionRunner, ActionRunner, IAction, IRunEvent } from 'vs/base/common/actions'; +import { ActionRunner, IAction, IRunEvent } from 'vs/base/common/actions'; import { Menu } from 'vs/base/browser/ui/menu/menu'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -23,10 +23,8 @@ export class ContextMenuHandler { private notificationService: INotificationService; private telemetryService: ITelemetryService; - private actionRunner: IActionRunner; private $el: Builder; private menuContainerElement: HTMLElement; - private toDispose: IDisposable[]; constructor(element: HTMLElement, contextViewService: IContextViewService, telemetryService: ITelemetryService, notificationService: INotificationService) { this.setContainer(element); @@ -35,41 +33,7 @@ export class ContextMenuHandler { this.telemetryService = telemetryService; this.notificationService = notificationService; - this.actionRunner = new ActionRunner(); this.menuContainerElement = null; - this.toDispose = []; - - let hideViewOnRun = false; - - this.toDispose.push(this.actionRunner.onDidBeforeRun((e: IRunEvent) => { - if (this.telemetryService) { - /* __GDPR__ - "workbenchActionExecuted" : { - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' }); - } - - hideViewOnRun = !!(e).retainActionItem; - - if (!hideViewOnRun) { - this.contextViewService.hideContextView(false); - } - })); - - this.toDispose.push(this.actionRunner.onDidRun((e: IRunEvent) => { - if (hideViewOnRun) { - this.contextViewService.hideContextView(false); - } - - hideViewOnRun = false; - - if (e.error && this.notificationService) { - this.notificationService.error(e.error); - } - })); } public setContainer(container: HTMLElement): void { @@ -102,24 +66,25 @@ export class ContextMenuHandler { container.className += ' ' + className; } - let menu = new Menu(container, actions, { + const menuDisposables: IDisposable[] = []; + + const actionRunner = delegate.actionRunner || new ActionRunner(); + actionRunner.onDidBeforeRun(this.onActionRun, this, menuDisposables); + actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables); + + const menu = new Menu(container, actions, { actionItemProvider: delegate.getActionItem, context: delegate.getActionsContext ? delegate.getActionsContext() : null, - actionRunner: this.actionRunner, + actionRunner, getKeyBinding: delegate.getKeyBinding }); - let listener1 = menu.onDidCancel(() => { - this.contextViewService.hideContextView(true); - }); - - let listener2 = menu.onDidBlur(() => { - this.contextViewService.hideContextView(true); - }); + menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables); + menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables); menu.focus(); - return combinedDisposable([listener1, listener2, menu]); + return combinedDisposable([...menuDisposables, menu]); }, onHide: (didCancel?: boolean) => { @@ -133,6 +98,26 @@ export class ContextMenuHandler { }); } + private onActionRun(e: IRunEvent): void { + if (this.telemetryService) { + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' }); + } + + this.contextViewService.hideContextView(false); + } + + private onDidActionRun(e: IRunEvent): void { + if (e.error && this.notificationService) { + this.notificationService.error(e.error); + } + } + private onMouseDown(e: MouseEvent): void { if (!this.menuContainerElement) { return; diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index ba2b3c673a2..f9667d4d695 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -27,12 +27,12 @@ class WindowRouter implements IClientRouter { constructor(private windowId: number) { } - routeCall(): string { - return `window:${this.windowId}`; + routeCall(): TPromise { + return TPromise.as(`window:${this.windowId}`); } - routeEvent(): string { - return `window:${this.windowId}`; + routeEvent(): TPromise { + return TPromise.as(`window:${this.windowId}`); } } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 99792a19f00..c291dff9fec 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -8,6 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface ParsedArgs { [arg: string]: any; _: string[]; + 'folder-uri'?: string | string[]; _urls?: string[]; help?: boolean; version?: boolean; @@ -29,7 +30,6 @@ export interface ParsedArgs { verbose?: boolean; log?: string; logExtensionHostCommunication?: boolean; - 'disable-extensions'?: boolean; 'extensions-dir'?: string; extensionDevelopmentPath?: string; extensionTestsPath?: string; @@ -38,6 +38,8 @@ export interface ParsedArgs { debugId?: string; debugSearch?: string; debugBrkSearch?: string; + 'disable-extensions'?: boolean; + 'disable-extension'?: string | string[]; 'list-extensions'?: boolean; 'show-versions'?: boolean; 'install-extension'?: string | string[]; @@ -100,7 +102,7 @@ export interface IEnvironmentService { workspacesHome: string; isExtensionDevelopment: boolean; - disableExtensions: boolean; + disableExtensions: boolean | string[]; extensionsPath: string; extensionDevelopmentPath: string; extensionTestsPath: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 3a8b2891705..30cfd3ddd91 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -18,9 +18,11 @@ const options: minimist.Opts = { 'locale', 'user-data-dir', 'extensions-dir', + 'folder-uri', 'extensionDevelopmentPath', 'extensionTestsPath', 'install-extension', + 'disable-extension', 'uninstall-extension', 'debugId', 'debugPluginHost', @@ -143,6 +145,7 @@ export function parseArgs(args: string[]): ParsedArgs { const optionsHelp: { [name: string]: string; } = { '-d, --diff ': localize('diff', "Compare two files with each other."), + '--folder-uri ': localize('folder uri', "Opens a window with given folder uri(s)"), '-a, --add

': localize('add', "Add folder(s) to the last active window."), '-g, --goto ': localize('goto', "Open a file at the path on the specified line and character position."), '-n, --new-window': localize('newWindow', "Force to open a new window."), @@ -170,6 +173,7 @@ const troubleshootingHelp: { [name: string]: string; } = { '-p, --performance': localize('performance', "Start with the 'Developer: Startup Performance' command enabled."), '--prof-startup': localize('prof-startup', "Run CPU profiler during startup"), '--disable-extensions': localize('disableExtensions', "Disable all installed extensions."), + '--disable-extension ': localize('disableExtension', "Disable an extension."), '--inspect-extensions': localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI."), '--inspect-brk-extensions': localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI."), '--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration."), diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 06e2171dfb5..9ef3481c43d 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -153,7 +153,21 @@ export class EnvironmentService implements IEnvironmentService { @memoize get extensionTestsPath(): string { return this._args.extensionTestsPath ? path.normalize(this._args.extensionTestsPath) : this._args.extensionTestsPath; } - get disableExtensions(): boolean { return this._args['disable-extensions']; } + get disableExtensions(): boolean | string[] { + if (this._args['disable-extensions']) { + return true; + } + const disableExtensions: string | string[] = this._args['disable-extension']; + if (disableExtensions) { + if (typeof disableExtensions === 'string') { + return [disableExtensions]; + } + if (Array.isArray(disableExtensions) && disableExtensions.length > 0) { + return disableExtensions; + } + } + return false; + } get skipGettingStarted(): boolean { return this._args['skip-getting-started']; } diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 28c6ce6805f..f56f3aabbbf 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, ILocalExtension, isIExtensionIdentifier, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getIdFromLocalExtensionId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getIdFromLocalExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -29,7 +29,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService { @IStorageService private storageService: IStorageService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IEnvironmentService private environmentService: IEnvironmentService, - @IExtensionManagementService extensionManagementService: IExtensionManagementService + @IExtensionManagementService private extensionManagementService: IExtensionManagementService ) { extensionManagementService.onDidUninstallExtension(this._onDidUninstallExtension, this, this.disposables); } @@ -38,7 +38,11 @@ export class ExtensionEnablementService implements IExtensionEnablementService { return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; } - getDisabledExtensions(): TPromise { + get allUserExtensionsDisabled(): boolean { + return this.environmentService.disableExtensions === true; + } + + async getDisabledExtensions(): Promise { let result = this._getDisabledExtensions(StorageScope.GLOBAL); @@ -54,14 +58,25 @@ export class ExtensionEnablementService implements IExtensionEnablementService { } } - return TPromise.as(result); + if (this.environmentService.disableExtensions) { + const allInstalledExtensions = await this.extensionManagementService.getInstalled(); + for (const installedExtension of allInstalledExtensions) { + if (this._isExtensionDisabledInEnvironment(installedExtension)) { + if (!result.some(r => areSameExtensions(r, installedExtension.galleryIdentifier))) { + result.push(installedExtension.galleryIdentifier); + } + } + } + } + + return result; } getEnablementState(extension: ILocalExtension): EnablementState { - if (this.environmentService.disableExtensions && extension.type === LocalExtensionType.User) { + if (this._isExtensionDisabledInEnvironment(extension)) { return EnablementState.Disabled; } - const identifier = this._getIdentifier(extension); + const identifier = extension.galleryIdentifier; if (this.hasWorkspace) { if (this._getEnabledExtensions(StorageScope.WORKSPACE).filter(e => areSameExtensions(e, identifier))[0]) { return EnablementState.WorkspaceEnabled; @@ -95,7 +110,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService { if (!this.canChangeEnablement(arg)) { return TPromise.wrap(false); } - identifier = this._getIdentifier(arg); + identifier = arg.galleryIdentifier; } const workspace = newState === EnablementState.WorkspaceDisabled || newState === EnablementState.WorkspaceEnabled; @@ -134,6 +149,17 @@ export class ExtensionEnablementService implements IExtensionEnablementService { return enablementState === EnablementState.WorkspaceEnabled || enablementState === EnablementState.Enabled; } + private _isExtensionDisabledInEnvironment(extension: ILocalExtension): boolean { + if (this.allUserExtensionsDisabled) { + return extension.type === LocalExtensionType.User; + } + const disabledExtensions = this.environmentService.disableExtensions; + if (Array.isArray(disabledExtensions)) { + return disabledExtensions.some(id => areSameExtensions({ id }, extension.galleryIdentifier)); + } + return false; + } + private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { if (this.hasWorkspace) { if (this._getEnabledExtensions(StorageScope.WORKSPACE).filter(e => areSameExtensions(e, identifier))[0]) { @@ -150,10 +176,6 @@ export class ExtensionEnablementService implements IExtensionEnablementService { return EnablementState.Enabled; } - private _getIdentifier(extension: ILocalExtension): IExtensionIdentifier { - return { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }; - } - private _enableExtension(identifier: IExtensionIdentifier): void { this._removeFromDisabledExtensions(identifier, StorageScope.WORKSPACE); this._removeFromEnabledExtensions(identifier, StorageScope.WORKSPACE); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 25e0dcdb540..e30d7e8787e 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -96,7 +96,7 @@ export interface IColor { export interface IExtensionContributions { commands?: ICommand[]; - configuration?: IConfiguration; + configuration?: IConfiguration | IConfiguration[]; debuggers?: IDebugger[]; grammars?: IGrammar[]; jsonValidation?: IJSONValidation[]; @@ -124,6 +124,7 @@ export interface IExtensionManifest { categories?: string[]; activationEvents?: string[]; extensionDependencies?: string[]; + extensionPack?: string[]; contributes?: IExtensionContributions; repository?: { url: string; @@ -135,6 +136,7 @@ export interface IExtensionManifest { export interface IGalleryExtensionProperties { dependencies?: string[]; + extensionPack?: string[]; engine?: string; } @@ -205,6 +207,7 @@ export enum LocalExtensionType { export interface ILocalExtension { type: LocalExtensionType; identifier: IExtensionIdentifier; + galleryIdentifier: IExtensionIdentifier; manifest: IExtensionManifest; metadata: IGalleryMetadata; location: URI; @@ -303,10 +306,10 @@ export interface IExtensionManagementService { onUninstallExtension: Event; onDidUninstallExtension: Event; - install(zipPath: string): TPromise; - installFromGallery(extension: IGalleryExtension): TPromise; + install(zipPath: string): TPromise; + installFromGallery(extension: IGalleryExtension): TPromise; uninstall(extension: ILocalExtension, force?: boolean): TPromise; - reinstallFromGallery(extension: ILocalExtension): TPromise; + reinstallFromGallery(extension: ILocalExtension): TPromise; getInstalled(type?: LocalExtensionType): TPromise; getExtensionsReport(): TPromise; @@ -340,6 +343,8 @@ export const IExtensionEnablementService = createDecorator; + getDisabledExtensions(): Promise; /** * Returns the enablement state for the given extension diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 1de227bfe06..4180af7c8a3 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -17,10 +17,10 @@ export interface IExtensionManagementChannel extends IChannel { 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: 'install', args: [string]): TPromise; + call(command: 'installFromGallery', args: [IGalleryExtension]): TPromise; call(command: 'uninstall', args: [ILocalExtension, boolean]): TPromise; - call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise; + call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise; call(command: 'getInstalled', args: [LocalExtensionType]): TPromise; call(command: 'getExtensionsReport'): TPromise; call(command: 'updateMetadata', args: [ILocalExtension, IGalleryMetadata]): TPromise; @@ -55,15 +55,19 @@ export class ExtensionManagementChannel implements IExtensionManagementChannel { switch (command) { 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(); } throw new Error('Invalid call'); } + + private _transform(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: URI.revive(extension.location) } } : extension; + } } export class ExtensionManagementChannelClient implements IExtensionManagementService { @@ -73,45 +77,46 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer constructor(private channel: IExtensionManagementChannel, private uriTransformer: IURITransformer) { } 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 onDidInstallExtension(): Event { return mapEvent(this.channel.listen('onDidInstallExtension'), i => ({ ...i, local: this._transformIncoming(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]) - .then(extension => this._transform(extension)); + install(zipPath: string): TPromise { + return this.channel.call('install', [zipPath]); } - installFromGallery(extension: IGalleryExtension): TPromise { - return this.channel.call('installFromGallery', [extension]) - .then(extension => this._transform(extension)); + installFromGallery(extension: IGalleryExtension): TPromise { + return this.channel.call('installFromGallery', [extension]); } uninstall(extension: ILocalExtension, force = false): TPromise { - return this.channel.call('uninstall', [extension, force]); + return this.channel.call('uninstall', [this._transformOutgoing(extension), force]); } - reinstallFromGallery(extension: ILocalExtension): TPromise { - return this.channel.call('reinstallFromGallery', [extension]) - .then(extension => this._transform(extension)); + reinstallFromGallery(extension: ILocalExtension): TPromise { + return this.channel.call('reinstallFromGallery', [this._transformOutgoing(extension)]); } getInstalled(type: LocalExtensionType = null): TPromise { return this.channel.call('getInstalled', [type]) - .then(extensions => extensions.map(extension => this._transform(extension))); + .then(extensions => extensions.map(extension => this._transformIncoming(extension))); } updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise { - return this.channel.call('updateMetadata', [local, metadata]) - .then(extension => this._transform(extension)); + return this.channel.call('updateMetadata', [this._transformOutgoing(local), metadata]) + .then(extension => this._transformIncoming(extension)); } getExtensionsReport(): TPromise { return this.channel.call('getExtensionsReport'); } - private _transform(extension: ILocalExtension): ILocalExtension { + private _transformIncoming(extension: ILocalExtension): ILocalExtension { return extension ? { ...extension, ...{ location: URI.revive(this.uriTransformer.transformIncoming(extension.location)) } } : extension; } + private _transformOutgoing(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: this.uriTransformer.transformOutgoing(extension.location) } } : extension; + } + } \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts b/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts index a7938100654..c9308a0d0f1 100644 --- a/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/multiExtensionManagement.ts @@ -41,7 +41,7 @@ export class MulitExtensionManagementService implements IExtensionManagementServ return this.getServer(extension).extensionManagementService.uninstall(extension, force); } - reinstallFromGallery(extension: ILocalExtension): TPromise { + reinstallFromGallery(extension: ILocalExtension): TPromise { return this.getServer(extension).extensionManagementService.reinstallFromGallery(extension); } @@ -49,12 +49,12 @@ export class MulitExtensionManagementService implements IExtensionManagementServ return this.getServer(extension).extensionManagementService.updateMetadata(extension, metadata); } - install(zipPath: string): TPromise { + install(zipPath: string): TPromise { return this.servers[0].extensionManagementService.install(zipPath); } - installFromGallery(extension: IGalleryExtension): TPromise { - return this.servers[0].extensionManagementService.installFromGallery(extension); + installFromGallery(extension: IGalleryExtension): TPromise { + return TPromise.join(this.servers.map(server => server.extensionManagementService.installFromGallery(extension))).then(() => null); } getExtensionsReport(): TPromise { diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index 87c15f9be41..16af4dbf97a 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -115,6 +115,7 @@ const AssetType = { const PropertyType = { Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies', + ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine' }; @@ -262,8 +263,8 @@ function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IG }; } -function getDependencies(version: IRawGalleryExtensionVersion): string[] { - const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Dependency) : []; +function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] { + const values = version.properties ? version.properties.filter(p => p.key === property) : []; const value = values.length > 0 && values[0].value; return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : []; } @@ -308,7 +309,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUr ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'), assets, properties: { - dependencies: getDependencies(version), + dependencies: getExtensions(version, PropertyType.Dependency), + extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version) }, /* __GDPR__FRAGMENT__ @@ -568,7 +570,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return this.getLastValidExtensionVersion(rawExtension, rawExtension.versions) .then(rawVersion => { if (rawVersion) { - extension.properties.dependencies = getDependencies(rawVersion); + extension.properties.dependencies = getExtensions(rawVersion, PropertyType.Dependency); extension.properties.engine = getEngine(rawVersion); extension.assets.download = getVersionAsset(rawVersion, AssetType.VSIX); extension.version = rawVersion.version; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 4cdee22f254..5cc1c5b5bb7 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -11,9 +11,9 @@ import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { flatten, distinct } from 'vs/base/common/arrays'; +import { flatten } from 'vs/base/common/arrays'; import { extract, buffer, ExtractError } from 'vs/base/node/zip'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IExtensionManifest, IGalleryMetadata, @@ -53,6 +53,7 @@ const INSTALL_ERROR_LOCAL = 'local'; const INSTALL_ERROR_EXTRACTING = 'extracting'; const INSTALL_ERROR_RENAMING = 'renaming'; const INSTALL_ERROR_DELETING = 'deleting'; +const INSTALL_ERROR_MALICIOUS = 'malicious'; const ERROR_UNKNOWN = 'unknown'; export class ExtensionManagementError extends Error { @@ -112,8 +113,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private uninstalledFileLimiter: Limiter; private reportedExtensions: TPromise | undefined; private lastReportTimestamp = 0; - private readonly installationStartTime: Map = new Map(); - private readonly installingExtensions: Map> = new Map>(); + private readonly installingExtensions: Map> = new Map>(); private readonly uninstallingExtensions: Map> = new Map>(); private readonly manifestCache: ExtensionsManifestCache; private readonly extensionLifecycle: ExtensionsLifecycle; @@ -152,14 +152,14 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - install(zipPath: string): TPromise { + install(zipPath: string): TPromise { zipPath = path.resolve(zipPath); return validateLocalExtension(zipPath) .then(manifest => { const identifier = { id: getLocalExtensionIdFromManifest(manifest) }; if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode)) { - return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); + return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); } return this.removeIfExists(identifier.id) .then( @@ -173,7 +173,7 @@ export class ExtensionManagementService extends Disposable implements IExtension metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest), error => this.installFromZipPath(identifier, zipPath, null, manifest)) .then( - local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, + () => { this.logService.info('Successfully installed the extension:', identifier.id); }, e => { this.logService.error('Failed to install the extension:', identifier.id, e.message); return TPromise.wrapError(e); @@ -219,20 +219,7 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(installed => { const operation = this.getOperation({ id: getIdFromLocalExtensionId(identifier.id), uuid: identifier.uuid }, installed); return this.installExtension({ zipPath, id: identifier.id, metadata }) - .then(local => { - if (this.galleryService.isEnabled() && local.manifest.extensionDependencies && local.manifest.extensionDependencies.length) { - return this.getDependenciesToInstall(local.manifest.extensionDependencies) - .then(dependenciesToInstall => { - dependenciesToInstall = metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall; - return this.downloadAndInstallExtensions(dependenciesToInstall, dependenciesToInstall.map(d => this.getOperation(d.identifier, installed))); - }) - .then(() => local, error => { - this.setUninstalled(local); - return TPromise.wrapError(new Error(nls.localize('errorInstallingDependencies', "Error while installing dependencies. {0}", error instanceof Error ? error.message : error))); - }); - } - return local; - }) + .then(local => this.installDependenciesAndPackExtensions(local, null).then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error)))) .then( local => { this._onDidInstallExtension.fire({ identifier, zipPath, local, operation }); return local; }, error => { this._onDidInstallExtension.fire({ identifier, zipPath, operation, error }); return TPromise.wrapError(error); } @@ -240,26 +227,60 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - installFromGallery(extension: IGalleryExtension): TPromise { - this.onInstallExtensions([extension]); - return this.toNonCancellablePromise(this.getInstalled(LocalExtensionType.User) - .then(installed => this.collectExtensionsToInstall(extension) - .then( - extensionsToInstall => { - if (extensionsToInstall.length > 1) { - this.onInstallExtensions(extensionsToInstall.slice(1)); - } - const operataions: InstallOperation[] = extensionsToInstall.map(e => this.getOperation(e.identifier, installed)); - return this.downloadAndInstallExtensions(extensionsToInstall, operataions) - .then( - locals => this.onDidInstallExtensions(extensionsToInstall, locals, operataions, []) - .then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier))[0]), - errors => this.onDidInstallExtensions(extensionsToInstall, [], operataions, errors)); - }, - error => this.onDidInstallExtensions([extension], [], [this.getOperation(extension.identifier, installed)], [error])))); + installFromGallery(extension: IGalleryExtension): TPromise { + let installingExtension = this.installingExtensions.get(extension.identifier.id); + if (!installingExtension) { + + let successCallback: ValueCallback, errorCallback: ErrorCallback; + installingExtension = new TPromise((c, e) => { successCallback = c; errorCallback = e; }); + this.installingExtensions.set(extension.identifier.id, installingExtension); + + try { + const startTime = new Date().getTime(); + const identifier = { id: getLocalExtensionIdFromGallery(extension, extension.version), uuid: extension.identifier.uuid }; + const telemetryData = getGalleryExtensionTelemetryData(extension); + let operation: InstallOperation = InstallOperation.Install; + + this.logService.info('Installing extension:', extension.name); + this._onInstallExtension.fire({ identifier, gallery: extension }); + + this.checkMalicious(extension) + .then(() => this.getInstalled(LocalExtensionType.User)) + .then(installed => { + const existingExtension = installed.filter(i => areSameExtensions(i.galleryIdentifier, extension.identifier))[0]; + operation = existingExtension ? InstallOperation.Update : InstallOperation.Install; + return this.downloadInstallableExtension(extension, operation) + .then(installableExtension => this.installExtension(installableExtension)) + .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) + .then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error)))); + }) + .then( + local => { + this.installingExtensions.delete(extension.identifier.id); + this.logService.info(`Extensions installed successfully:`, extension.identifier.id); + this._onDidInstallExtension.fire({ identifier, gallery: extension, local, operation }); + this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, void 0); + successCallback(null); + }, + error => { + this.installingExtensions.delete(extension.identifier.id); + const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; + this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode); + this._onDidInstallExtension.fire({ identifier, gallery: extension, operation, error: errorCode }); + this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, error); + errorCallback(error); + }); + + } catch (error) { + this.installingExtensions.delete(extension.identifier.id); + errorCallback(error); + } + + } + return installingExtension; } - reinstallFromGallery(extension: ILocalExtension): TPromise { + reinstallFromGallery(extension: ILocalExtension): TPromise { if (!this.galleryService.isEnabled()) { return TPromise.wrapError(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); } @@ -280,46 +301,19 @@ export class ExtensionManagementService extends Disposable implements IExtension return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall)) ? InstallOperation.Update : InstallOperation.Install; } - private collectExtensionsToInstall(extension: IGalleryExtension): TPromise { - return this.galleryService.loadCompatibleVersion(extension) - .then(compatible => { - if (!compatible) { - return TPromise.wrapError(new ExtensionManagementError(nls.localize('notFoundCompatible', "Unable to install '{0}'; there is no available version compatible with VS Code '{1}'.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE)); + private getTelemetryEvent(operation: InstallOperation): string { + return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install'; + } + + private checkMalicious(extension: IGalleryExtension): TPromise { + return this.getExtensionsReport() + .then(report => { + if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { + throw new ExtensionManagementError(INSTALL_ERROR_MALICIOUS, nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); + } else { + return null; } - return this.getDependenciesToInstall(compatible.properties.dependencies) - .then( - dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]), - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); - }, - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); - } - - private downloadAndInstallExtensions(extensions: IGalleryExtension[], operations: InstallOperation[]): TPromise { - return TPromise.join(extensions.map((extensionToInstall, index) => this.downloadAndInstallExtension(extensionToInstall, operations[index]))) - .then(null, errors => this.rollback(extensions).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors))); - } - - private downloadAndInstallExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise { - let installingExtension = this.installingExtensions.get(extension.identifier.id); - if (!installingExtension) { - installingExtension = this.getExtensionsReport() - .then(report => { - if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { - throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); - } else { - return extension; - } - }) - .then(extension => this.downloadInstallableExtension(extension, operation)) - .then(installableExtension => this.installExtension(installableExtension)) - .then( - local => { this.installingExtensions.delete(extension.identifier.id); return local; }, - e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); } - ); - - this.installingExtensions.set(extension.identifier.id, installingExtension); - } - return installingExtension; + }); } private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise { @@ -352,54 +346,6 @@ export class ExtensionManagementService extends Disposable implements IExtension error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); } - private onInstallExtensions(extensions: IGalleryExtension[]): void { - for (const extension of extensions) { - this.logService.info('Installing extension:', extension.name); - this.installationStartTime.set(extension.identifier.id, new Date().getTime()); - const id = getLocalExtensionIdFromGallery(extension, extension.version); - this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension }); - } - } - - private onDidInstallExtensions(extensions: IGalleryExtension[], locals: ILocalExtension[], operations: InstallOperation[], errors: Error[]): TPromise { - extensions.forEach((gallery, index) => { - const identifier = { id: getLocalExtensionIdFromGallery(gallery, gallery.version), uuid: gallery.identifier.uuid }; - const local = locals[index]; - const error = errors[index]; - const operation = operations[index]; - if (local) { - this.logService.info(`Extensions installed successfully:`, gallery.identifier.id); - this._onDidInstallExtension.fire({ identifier, gallery, local, operation }); - } else { - const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; - this.logService.error(`Failed to install extension:`, gallery.identifier.id, error ? error.message : errorCode); - this._onDidInstallExtension.fire({ identifier, gallery, operation, error: errorCode }); - } - const startTime = this.installationStartTime.get(gallery.identifier.id); - this.reportTelemetry(operations[index] === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(gallery), startTime ? new Date().getTime() - startTime : void 0, error); - this.installationStartTime.delete(gallery.identifier.id); - }); - return errors.length ? TPromise.wrapError(this.joinErrors(errors)) : TPromise.as(null); - } - - private getDependenciesToInstall(dependencies: string[]): TPromise { - if (dependencies.length) { - return this.getInstalled() - .then(installed => { - const uninstalledDeps = dependencies.filter(d => installed.every(i => getGalleryExtensionId(i.manifest.publisher, i.manifest.name) !== d)); - if (uninstalledDeps.length) { - return this.galleryService.loadAllDependencies(uninstalledDeps.map(id => ({ id }))) - .then(allDependencies => allDependencies.filter(d => { - const extensionId = getLocalExtensionIdFromGallery(d, d.version); - return installed.every(({ identifier }) => identifier.id !== extensionId); - })); - } - return []; - }); - } - return TPromise.as([]); - } - private installExtension(installableExtension: InstallableExtension): TPromise { return this.unsetUninstalledAndGetLocal(installableExtension.id) .then( @@ -486,11 +432,44 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } + private installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension): TPromise { + if (this.galleryService.isEnabled()) { + const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; + if (installed.manifest.extensionPack) { + for (const extension of installed.manifest.extensionPack) { + // add only those extensions which are new in currently installed extension + if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) { + if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) { + dependenciesAndPackExtensions.push(extension); + } + } + } + } + if (dependenciesAndPackExtensions.length) { + return this.getInstalled() + .then(installed => { + // filter out installing and installed extensions + const names = dependenciesAndPackExtensions.filter(id => !this.installingExtensions.has(adoptToGalleryExtensionId(id)) && installed.every(({ galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id }))); + if (names.length) { + return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }) + .then(galleryResult => { + const extensionsToInstall = galleryResult.firstPage; + return TPromise.join(extensionsToInstall.map(e => this.installFromGallery(e))) + .then(() => null, errors => this.rollback(extensionsToInstall).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors))); + }); + } + return null; + }); + } + } + return TPromise.as(null); + } + private rollback(extensions: IGalleryExtension[]): TPromise { return this.getInstalled(LocalExtensionType.User) .then(installed => TPromise.join(installed.filter(local => extensions.some(galleryExtension => local.identifier.id === getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version))) // Only check id (pub.name-version) because we want to rollback the exact version - .map(local => this.setUninstalled(local)))) + .map(local => this.uninstall(local, true)))) .then(() => null, () => null); } @@ -558,7 +537,14 @@ export class ExtensionManagementService extends Disposable implements IExtension private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise { return this.preUninstallExtension(extension) - .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.uninstallWithDependencies(extension, [], installed)) + .then(() => { + if (force) { + return this.uninstallExtensionAsPack(extension, installed); + } + const hasInstalledExtensionPack = extension.manifest.extensionPack && extension.manifest.extensionPack.length && installed.some(i => extension.manifest.extensionPack.some(dep => areSameExtensions({ id: dep }, i.galleryIdentifier))); + const hasDependencies = extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0; + return hasInstalledExtensionPack || hasDependencies ? this.promptForPackAndUninstall(extension, installed) : this.uninstallExtensions(extension, [], installed); + }) .then(() => this.postUninstallExtension(extension), error => { this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); @@ -566,47 +552,45 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean { - if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) { - return installed.some(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1); - } - return false; - } - - private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise { - if (force) { - const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension); - return this.uninstallWithDependencies(extension, dependencies, installed); - } - - const message = nls.localize('uninstallDependeciesConfirmation', "Would you like to uninstall '{0}' only or its dependencies also?", extension.manifest.displayName || extension.manifest.name); + private promptForPackAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): TPromise { + const message = nls.localize('uninstallExtensionPackConfirmation', "Would you like to uninstall '{0}' only or as a pack?", extension.manifest.displayName || extension.manifest.name); const buttons = [ - nls.localize('uninstallOnly', "Extension Only"), - nls.localize('uninstallAll', "Uninstall All"), + nls.localize('uninstallPack', "Uninstall Extension Pack"), + nls.localize('uninstallOnly', "Uninstall Extension Only"), nls.localize('cancel', "Cancel") ]; - this.logService.info('Requesting for confirmation to uninstall extension with dependencies', extension.identifier.id); return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 }) .then(value => { if (value === 0) { - return this.uninstallWithDependencies(extension, [], installed); + return this.uninstallExtensionAsPack(extension, installed); } if (value === 1) { - const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension); - return this.uninstallWithDependencies(extension, dependencies, installed); + return this.uninstallExtensions(extension, [], installed); } this.logService.info('Cancelled uninstalling extension:', extension.identifier.id); return TPromise.wrapError(errors.canceled()); }, error => TPromise.wrapError(errors.canceled())); } - private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise { - const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed); - let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1); - if (dependents.length) { - return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, dependents))); + private uninstallExtensionAsPack(extension: ILocalExtension, installed: ILocalExtension[]): TPromise { + const extensionsToUninstall = this.getDependenciesToUninstall(extension, installed); + for (const packExtensionToUninstall of this.getAllPackExtensionsToUninstall(extension, installed)) { + if (extensionsToUninstall.indexOf(packExtensionToUninstall) === -1) { + extensionsToUninstall.push(packExtensionToUninstall); + } } - return TPromise.join([this.uninstallExtension(extension), ...dependenciesToUninstall.map(d => this.doUninstall(d))]).then(() => null); + return this.uninstallExtensions(extension, extensionsToUninstall, installed); + } + + private uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): TPromise { + const dependents = this.getDependents(extension, installed); + if (dependents.length) { + const remainingDependents = dependents.filter(dependent => extension !== dependent && otherExtensionsToUninstall.indexOf(dependent) === -1); + if (remainingDependents.length) { + return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, remainingDependents))); + } + } + return TPromise.join([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]).then(() => null); } private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string { @@ -622,7 +606,23 @@ export class ExtensionManagementService extends Disposable implements IExtension extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name); } - private getDependenciesToUninstallRecursively(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[]): ILocalExtension[] { + private getDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] { + const dependencies = this.getAllDependenciesToUninstall(extension, installed).filter(e => e !== extension); + + const dependenciesToUninstall = dependencies.slice(0); + for (let index = 0; index < dependencies.length; index++) { + const dep = dependencies[index]; + const dependents = this.getDependents(dep, installed); + // Remove the dependency from the uninstall list if there is a dependent which will not be uninstalled. + if (dependents.some(e => e !== extension && dependencies.indexOf(e) === -1)) { + dependenciesToUninstall.splice(index - (dependencies.length - dependenciesToUninstall.length), 1); + } + } + + return dependenciesToUninstall; + } + + private getAllDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] { if (checked.indexOf(extension) !== -1) { return []; } @@ -630,29 +630,32 @@ export class ExtensionManagementService extends Disposable implements IExtension if (!extension.manifest.extensionDependencies || extension.manifest.extensionDependencies.length === 0) { return []; } - const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1); + const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.some(id => areSameExtensions({ id }, i.galleryIdentifier))); const depsOfDeps = []; for (const dep of dependenciesToUninstall) { - depsOfDeps.push(...this.getDependenciesToUninstallRecursively(dep, installed, checked)); + depsOfDeps.push(...this.getAllDependenciesToUninstall(dep, installed, checked)); } return [...dependenciesToUninstall, ...depsOfDeps]; } - private filterDependents(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): ILocalExtension[] { - installed = installed.filter(i => i !== extension && i.manifest.extensionDependencies && i.manifest.extensionDependencies.length > 0); - let result = dependencies.slice(0); - for (let i = 0; i < dependencies.length; i++) { - const dep = dependencies[i]; - const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1); - if (dependents.length) { - result.splice(i - (dependencies.length - result.length), 1); - } + private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] { + if (checked.indexOf(extension) !== -1) { + return []; } - return result; + checked.push(extension); + if (!extension.manifest.extensionPack || extension.manifest.extensionPack.length === 0) { + return []; + } + const packedExtensions = installed.filter(i => extension.manifest.extensionPack.some(id => areSameExtensions({ id }, i.galleryIdentifier))); + const packOfPackedExtensions = []; + for (const packedExtension of packedExtensions) { + packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked)); + } + return [...packedExtensions, ...packOfPackedExtensions]; } private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] { - return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(extension)) !== -1); + return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.galleryIdentifier))); } private doUninstall(extension: ILocalExtension): TPromise { @@ -761,8 +764,12 @@ export class ExtensionManagementService extends Disposable implements IExtension if (manifest.extensionDependencies) { manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id)); } + if (manifest.extensionPack) { + manifest.extensionPack = manifest.extensionPack.map(id => adoptToGalleryExtensionId(id)); + } const identifier = { id: type === LocalExtensionType.System ? folderName : getLocalExtensionIdFromManifest(manifest), uuid: metadata ? metadata.id : null }; - return { type, identifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl }; + const galleryIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier.uuid }; + return { type, identifier, galleryIdentifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl }; })) .then(null, () => null); } diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index 717a17dd3b0..655d4b05b50 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts @@ -35,10 +35,11 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { constructor(instantiationService: TestInstantiationService) { super(storageService(instantiationService), instantiationService.get(IWorkspaceContextService), instantiationService.get(IEnvironmentService) || instantiationService.stub(IEnvironmentService, {} as IEnvironmentService), - instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: new Emitter() })); + instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, + { onDidUninstallExtension: new Emitter().event } as IExtensionManagementService)); } - public reset(): TPromise { + public async reset(): Promise { return this.getDisabledExtensions().then(extensions => extensions.forEach(d => this.setEnablement(aLocalExtension(d.id), EnablementState.Enabled))); } } @@ -52,7 +53,7 @@ suite('ExtensionEnablementService Test', () => { setup(() => { instantiationService = new TestInstantiationService(); - instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event }); + instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => TPromise.as([]) } as IExtensionManagementService); testObject = new TestExtensionEnablementService(instantiationService); }); @@ -331,6 +332,12 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); + test('test canChangeEnablement return false when the extension is disabled in environment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); + }); + test('test canChangeEnablement return true for system extensions when extensions are disabled in environment', () => { instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -338,12 +345,32 @@ suite('ExtensionEnablementService Test', () => { extension.type = LocalExtensionType.System; assert.equal(testObject.canChangeEnablement(extension), true); }); + + test('test canChangeEnablement return false for system extensions when extension is disabled in environment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + testObject = new TestExtensionEnablementService(instantiationService); + const extension = aLocalExtension('pub.a'); + extension.type = LocalExtensionType.System; + assert.equal(testObject.canChangeEnablement(extension), true); + }); + + test('test getDisabledExtensions include extensions disabled in enviroment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => TPromise.as([aLocalExtension('pub.a'), aLocalExtension('pub.b')]) } as IExtensionManagementService); + testObject = new TestExtensionEnablementService(instantiationService); + return testObject.getDisabledExtensions() + .then(actual => { + assert.equal(actual.length, 1); + assert.equal(actual[0].id, 'pub.a'); + }); + }); }); function aLocalExtension(id: string, contributes?: IExtensionContributions): ILocalExtension { const [publisher, name] = id.split('.'); return Object.create({ identifier: { id }, + galleryIdentifier: { id, uuid: void 0 }, manifest: { name, publisher, diff --git a/src/vs/platform/history/common/history.ts b/src/vs/platform/history/common/history.ts index 1ce03f870fc..f09d7fd44ba 100644 --- a/src/vs/platform/history/common/history.ts +++ b/src/vs/platform/history/common/history.ts @@ -24,7 +24,7 @@ export interface IHistoryMainService { addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void; getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened; - removeFromRecentlyOpened(paths: string[]): void; + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): void; clearRecentlyOpened(): void; updateWindowsJumpList(): void; diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 8b8dbc2fe10..2df1458fbd9 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -16,11 +16,19 @@ import { getPathLabel, getBaseLabel } from 'vs/base/common/labels'; import { IPath } from 'vs/platform/windows/common/windows'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceSavedEvent } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, IWorkspaceSavedEvent, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isEqual } from 'vs/base/common/paths'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { getComparisonKey, isEqual as areResourcesEqual, hasToIgnoreCase } from 'vs/base/common/resources'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; + +interface ISerializedRecentlyOpened { + workspaces: (IWorkspaceIdentifier | string | UriComponents)[]; + files: string[]; +} export class HistoryMainService implements IHistoryMainService { @@ -62,29 +70,33 @@ export class HistoryMainService implements IHistoryMainService { const mru = this.getRecentlyOpened(); // Workspaces - workspaces.forEach(workspace => { - const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace); - if (isUntitledWorkspace) { - return; // only store saved workspaces - } + if (Array.isArray(workspaces)) { + workspaces.forEach(workspace => { + const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace); + if (isUntitledWorkspace) { + return; // only store saved workspaces + } - mru.workspaces.unshift(workspace); - mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace)); + mru.workspaces.unshift(workspace); + mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace)); - // We do not add to recent documents here because on Windows we do this from a custom - // JumpList and on macOS we fill the recent documents in one go from all our data later. - }); + // We do not add to recent documents here because on Windows we do this from a custom + // JumpList and on macOS we fill the recent documents in one go from all our data later. + }); + } // Files - files.forEach((path) => { - mru.files.unshift(path); - mru.files = arrays.distinct(mru.files, file => this.distinctFn(file)); + if (Array.isArray(files)) { + files.forEach((path) => { + mru.files.unshift(path); + mru.files = arrays.distinct(mru.files, file => this.distinctFn(file)); - // Add to recent documents (Windows only, macOS later) - if (isWindows) { - app.addRecentDocument(path); - } - }); + // Add to recent documents (Windows only, macOS later) + if (isWindows) { + app.addRecentDocument(path); + } + }); + } // Make sure its bounded mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES); @@ -100,21 +112,37 @@ export class HistoryMainService implements IHistoryMainService { } } - removeFromRecentlyOpened(pathsToRemove: string[]): void { + removeFromRecentlyOpened(pathsToRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): void { const mru = this.getRecentlyOpened(); let update = false; pathsToRemove.forEach((pathToRemove => { // Remove workspace - let index = arrays.firstIndex(mru.workspaces, workspace => isEqual(isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath, pathToRemove, !isLinux /* ignorecase */)); + let index = arrays.firstIndex(mru.workspaces, workspace => { + if (isWorkspaceIdentifier(pathToRemove)) { + return isWorkspaceIdentifier(workspace) && isEqual(pathToRemove.configPath, workspace.configPath, !isLinux /* ignorecase */); + } + if (isSingleFolderWorkspaceIdentifier(pathToRemove)) { + return isSingleFolderWorkspaceIdentifier(workspace) && areResourcesEqual(pathToRemove, workspace, hasToIgnoreCase(pathToRemove)); + } + if (typeof pathToRemove === 'string') { + if (isSingleFolderWorkspaceIdentifier(workspace)) { + return workspace.scheme === Schemas.file && areResourcesEqual(URI.file(pathToRemove), workspace, hasToIgnoreCase(workspace)); + } + if (isWorkspaceIdentifier(workspace)) { + return isEqual(pathToRemove, workspace.configPath, !isLinux /* ignorecase */); + } + } + return false; + }); if (index >= 0) { mru.workspaces.splice(index, 1); update = true; } // Remove file - index = arrays.firstIndex(mru.files, file => isEqual(file, pathToRemove, !isLinux /* ignorecase */)); + index = arrays.firstIndex(mru.files, file => typeof pathToRemove === 'string' && isEqual(file, pathToRemove, !isLinux /* ignorecase */)); if (index >= 0) { mru.files.splice(index, 1); update = true; @@ -151,7 +179,7 @@ export class HistoryMainService implements IHistoryMainService { // Take up to maxEntries/2 workspaces for (let i = 0; i < mru.workspaces.length && i < HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES / 2; i++) { const workspace = mru.workspaces[i]; - app.addRecentDocument(isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath); + app.addRecentDocument(isSingleFolderWorkspaceIdentifier(workspace) ? workspace.scheme === Schemas.file ? workspace.fsPath : workspace.toString() : workspace.configPath); maxEntries--; } @@ -175,7 +203,7 @@ export class HistoryMainService implements IHistoryMainService { let files: string[]; // Get from storage - const storedRecents = this.stateService.getItem(HistoryMainService.recentlyOpenedStorageKey); + const storedRecents = this.getRecentlyOpenedFromStorage(); if (storedRecents) { workspaces = storedRecents.workspaces || []; files = storedRecents.files || []; @@ -206,13 +234,39 @@ export class HistoryMainService implements IHistoryMainService { private distinctFn(workspaceOrFile: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string): string { if (isSingleFolderWorkspaceIdentifier(workspaceOrFile)) { + return getComparisonKey(workspaceOrFile); + } + if (typeof workspaceOrFile === 'string') { return isLinux ? workspaceOrFile : workspaceOrFile.toLowerCase(); } return workspaceOrFile.id; } + private getRecentlyOpenedFromStorage(): IRecentlyOpened { + const storedRecents: ISerializedRecentlyOpened = this.stateService.getItem(HistoryMainService.recentlyOpenedStorageKey) || { workspaces: [], files: [] }; + const result: IRecentlyOpened = { workspaces: [], files: storedRecents.files }; + for (const workspace of storedRecents.workspaces) { + if (typeof workspace === 'string') { + result.workspaces.push(URI.file(workspace)); + } else if (isWorkspaceIdentifier(workspace)) { + result.workspaces.push(workspace); + } else { + result.workspaces.push(URI.revive(workspace)); + } + } + return result; + } + private saveRecentlyOpened(recent: IRecentlyOpened): void { + const serialized: ISerializedRecentlyOpened = { workspaces: [], files: recent.files }; + for (const workspace of recent.workspaces) { + if (isSingleFolderWorkspaceIdentifier(workspace)) { + serialized.workspaces.push(workspace.toJSON()); + } else { + serialized.workspaces.push(workspace); + } + } this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, recent); } @@ -253,15 +307,26 @@ export class HistoryMainService implements IHistoryMainService { type: 'custom', name: nls.localize('recentFolders', "Recent Workspaces"), items: this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(workspace => { - const title = isSingleFolderWorkspaceIdentifier(workspace) ? getBaseLabel(workspace) : getWorkspaceLabel(workspace, this.environmentService); - const description = isSingleFolderWorkspaceIdentifier(workspace) ? nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(path.dirname(workspace), this.environmentService)) : nls.localize('codeWorkspace', "Code Workspace"); + const title = getWorkspaceLabel(workspace, this.environmentService); + const description = isSingleFolderWorkspaceIdentifier(workspace) ? nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(path.dirname(workspace.path), this.environmentService)) : nls.localize('codeWorkspace', "Code Workspace"); + let args; + // use quotes to support paths with whitespaces + if (isSingleFolderWorkspaceIdentifier(workspace)) { + if (workspace.scheme === Schemas.file) { + args = `"${workspace.fsPath}"`; + } else { + args = `--folderUri "${workspace.path}"`; + } + } else { + args = `"${workspace.configPath}"`; + } return { type: 'task', title, description, program: process.execPath, - args: `"${isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath}"`, // open folder (use quotes to support paths with whitespaces) + args, iconPath: 'explorer.exe', // simulate folder icon iconIndex: 0 }; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index bc246f6ed0c..9bd6cae4587 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; +import { ITree, ITreeConfiguration, ITreeOptions, IRenderer as ITreeRenderer } from 'vs/base/parts/tree/browser/tree'; import { List, IListOptions, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, IMultipleSelectionController, IOpenController, DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; -import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; -import { attachListStyler, defaultListStyles, computeStyles } from 'vs/platform/theme/common/styler'; +import { attachListStyler, defaultListStyles, computeStyles, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,8 +23,14 @@ import { DefaultController, IControllerOptions, OpenMode, ClickBehavior, Default import { isUndefinedOrNull } from 'vs/base/common/types'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { Event, Emitter } from 'vs/base/common/event'; -import { createStyleSheet } from 'vs/base/browser/dom'; +import { createStyleSheet, addStandardDisposableListener, getTotalHeight, removeClass, addClass } from 'vs/base/browser/dom'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { onUnexpectedError, canceled } from 'vs/base/common/errors'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; export type ListWidget = List | PagedList | ITree; @@ -90,16 +96,12 @@ export class ListService implements IListService { const RawWorkbenchListFocusContextKey = new RawContextKey('listFocus', true); export const WorkbenchListSupportsMultiSelectContextKey = new RawContextKey('listSupportsMultiselect', true); export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListFocusContextKey, ContextKeyExpr.not(InputFocusedContextKey)); +export const WorkbenchListHasSelectionOrFocus = new RawContextKey('listHasSelectionOrFocus', false); export const WorkbenchListDoubleSelection = new RawContextKey('listDoubleSelection', false); export const WorkbenchListMultiSelection = new RawContextKey('listMultiSelection', false); function createScopedContextKeyService(contextKeyService: IContextKeyService, widget: ListWidget): IContextKeyService { const result = contextKeyService.createScoped(widget.getHTMLElement()); - - if (widget instanceof List || widget instanceof PagedList) { - WorkbenchListSupportsMultiSelectContextKey.bindTo(result); - } - RawWorkbenchListFocusContextKey.bindTo(result); return result; } @@ -199,6 +201,7 @@ export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; + private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -206,7 +209,7 @@ export class WorkbenchList extends List { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IRenderer[], options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -225,6 +228,11 @@ export class WorkbenchList extends List { ); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + + const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); + + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -236,8 +244,17 @@ export class WorkbenchList extends List { attachListStyler(this, themeService), this.onSelectionChange(() => { const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); this.listMultiSelection.set(selection.length > 1); this.listDoubleSelection.set(selection.length === 2); + }), + this.onFocusChange(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); }) ])); @@ -267,7 +284,7 @@ export class WorkbenchPagedList extends PagedList { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IPagedRenderer[], options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -287,6 +304,9 @@ export class WorkbenchPagedList extends PagedList { this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); this.disposables.push(combinedDisposable([ @@ -323,6 +343,7 @@ export class WorkbenchTree extends Tree { protected disposables: IDisposable[]; + private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -352,6 +373,10 @@ export class WorkbenchTree extends Tree { this.disposables = []; this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + + WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -366,10 +391,20 @@ export class WorkbenchTree extends Tree { this.disposables.push(this.onDidChangeSelection(() => { const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus); this.listDoubleSelection.set(selection && selection.length === 2); this.listMultiSelection.set(selection && selection.length > 1); })); + this.disposables.push(this.onDidChangeFocus(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus); + })); + this.disposables.push(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(openModeSettingKey)) { this._openOnSingleClick = useSingleClickToOpen(configurationService); @@ -526,6 +561,169 @@ export class TreeResourceNavigator extends Disposable { } } + +export interface IHighlightingRenderer extends ITreeRenderer { + /** + * Update hightlights and return the best matching element + */ + updateHighlights(tree: ITree, pattern: string): any; +} + +export interface IHighlightingTreeConfiguration extends ITreeConfiguration { + renderer: IHighlightingRenderer & ITreeRenderer; +} + +export class HighlightingTreeController extends WorkbenchTreeController { + + constructor( + options: IControllerOptions, + private readonly onType: () => any, + @IConfigurationService configurationService: IConfigurationService + ) { + super(options, configurationService); + } + + onKeyDown(tree: ITree, event: IKeyboardEvent) { + let handled = super.onKeyDown(tree, event); + if (handled) { + return true; + } + if (this.upKeyBindingDispatcher.has(event.keyCode)) { + return false; + } + if (event.ctrlKey || event.metaKey) { + // ignore ctrl/cmd-combination but not shift/alt-combinatios + return false; + } + // crazy -> during keydown focus moves to the input box + // and because of that the keyup event is handled by the + // input field + if (event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z) { + // todo@joh this is much weaker than using the KeyboardMapperFactory + // but due to layering-challanges that's not available here... + this.onType(); + return true; + } + return false; + } +} + +export class HighlightingWorkbenchTree extends WorkbenchTree { + + protected readonly domNode: HTMLElement; + protected readonly inputContainer: HTMLElement; + protected readonly input: InputBox; + + protected readonly renderer: IHighlightingRenderer; + + constructor( + parent: HTMLElement, + treeConfiguration: IHighlightingTreeConfiguration, + treeOptions: ITreeOptions, + listOptions: IInputOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextViewService contextViewService: IContextViewService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + // build html skeleton + const container = document.createElement('div'); + container.className = 'highlighting-tree'; + const inputContainer = document.createElement('div'); + inputContainer.className = 'input'; + const treeContainer = document.createElement('div'); + treeContainer.className = 'tree'; + container.appendChild(inputContainer); + container.appendChild(treeContainer); + parent.appendChild(container); + + // create tree + treeConfiguration.controller = treeConfiguration.controller || instantiationService.createInstance(HighlightingTreeController, {}, () => this.onTypeInTree()); + super(treeContainer, treeConfiguration, treeOptions, contextKeyService, listService, themeService, instantiationService, configurationService); + this.renderer = treeConfiguration.renderer; + + this.domNode = container; + addClass(this.domNode, 'inactive'); + + // create input + this.inputContainer = inputContainer; + this.input = new InputBox(inputContainer, contextViewService, listOptions); + this.input.setEnabled(false); + this.input.onDidChange(this.updateHighlights, this, this.disposables); + this.disposables.push(attachInputBoxStyler(this.input, themeService)); + this.disposables.push(this.input); + this.disposables.push(addStandardDisposableListener(this.input.inputElement, 'keydown', event => { + //todo@joh make this command/context-key based + switch (event.keyCode) { + case KeyCode.DownArrow: + case KeyCode.UpArrow: + this.domFocus(); + break; + case KeyCode.Enter: + case KeyCode.Tab: + this.setSelection(this.getSelection()); + break; + case KeyCode.Escape: + this.input.value = ''; + this.domFocus(); + break; + } + })); + } + + setInput(element: any): TPromise { + this.input.setEnabled(false); + return super.setInput(element).then(value => { + if (!this.input.inputElement) { + // has been disposed in the meantime -> cancel + return Promise.reject(canceled()); + } + this.input.setEnabled(true); + return value; + }); + } + + layout(height?: number, width?: number): void { + this.input.layout(); + super.layout(isNaN(height) ? height : height - getTotalHeight(this.inputContainer), width); + } + + private onTypeInTree(): void { + removeClass(this.domNode, 'inactive'); + this.input.focus(); + this.layout(); + } + + private lastSelection: any[]; + + private updateHighlights(pattern: string): void { + + // remember old selection + let defaultSelection: any[]; + if (!this.lastSelection && pattern) { + this.lastSelection = this.getSelection(); + defaultSelection = []; + } else if (this.lastSelection && !pattern) { + defaultSelection = this.lastSelection; + this.lastSelection = []; + } + + let topElement = this.renderer.updateHighlights(this, pattern); + if (topElement && pattern) { + this.reveal(topElement).then(_ => { + this.setSelection([topElement], this); + this.setFocus(topElement, this); + return this.refresh(); + }, onUnexpectedError); + } else { + this.setSelection(defaultSelection, this); + this.refresh().then(undefined, onUnexpectedError); + } + } +} + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ 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/bufferLog.ts b/src/vs/platform/log/common/bufferLog.ts index 65fa32adb0e..437fd54d2f5 100644 --- a/src/vs/platform/log/common/bufferLog.ts +++ b/src/vs/platform/log/common/bufferLog.ts @@ -30,6 +30,15 @@ export class BufferLogService extends AbstractLogService implements ILogService private buffer: ILog[] = []; private _logger: ILogService | undefined = undefined; + constructor() { + super(); + this._register(this.onDidChangeLogLevel(level => { + if (this._logger) { + this._logger.setLevel(level); + } + })); + } + set logger(logger: ILogService) { this._logger = logger; diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 91ad26c83c2..d2613c57504 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -24,7 +24,7 @@ export const ISearchService = createDecorator('searchService'); */ export interface ISearchService { _serviceBrand: any; - search(query: ISearchQuery): PPromise; + search(query: ISearchQuery, onProgress?: (result: ISearchProgressItem) => void): TPromise; extendQuery(query: ISearchQuery): void; clearCache(cacheKey: string): TPromise; registerSearchResultProvider(scheme: string, provider: ISearchResultProvider): IDisposable; diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 77d9c406d30..5238541eac1 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -6,7 +6,7 @@ 'use strict'; import * as path from 'path'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { writeFileAndFlushSync } from 'vs/base/node/extfs'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 3d75158aa79..a68ff28367b 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -12,6 +12,7 @@ import URI from 'vs/base/common/uri'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; export const NullTelemetryService = new class implements ITelemetryService { _serviceBrand: undefined; @@ -42,6 +43,27 @@ export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetry export const NullAppender: ITelemetryAppender = { log: () => null, dispose: () => TPromise.as(null) }; + +export class LogAppender implements ITelemetryAppender { + + private commonPropertiesRegex = /^sessionID$|^version$|^timestamp$|^commitHash$|^common\./; + constructor(@ILogService private readonly _logService: ILogService) { } + + dispose(): TPromise { + return TPromise.as(undefined); + } + + log(eventName: string, data: any): void { + const strippedData = {}; + Object.keys(data).forEach(key => { + if (!this.commonPropertiesRegex.test(key)) { + strippedData[key] = data[key]; + } + }); + this._logService.trace(`telemetry/${eventName}`, strippedData); + } +} + /* __GDPR__FRAGMENT__ "URIDescriptor" : { "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -130,35 +152,39 @@ const configurationValueWhitelist = [ 'editor.formatOnSave', 'editor.colorDecorators', - 'window.zoomLevel', - 'files.autoSave', - 'files.hotExit', + 'breadcrumbs.enabled', + 'breadcrumbs.filePath', + 'breadcrumbs.symbolPath', + 'breadcrumbs.useQuickPick', + 'explorer.openEditors.visible', + 'extensions.autoUpdate', 'files.associations', - 'workbench.statusBar.visible', + 'files.autoGuessEncoding', + 'files.autoSave', + 'files.autoSaveDelay', + 'files.encoding', + 'files.eol', + 'files.hotExit', 'files.trimTrailingWhitespace', 'git.confirmSync', - 'workbench.sideBar.location', - 'window.openFilesInNewWindow', - 'javascript.validate.enable', - 'window.restoreWindows', - 'extensions.autoUpdate', - 'files.eol', - 'explorer.openEditors.visible', - 'workbench.editor.enablePreview', - 'files.autoSaveDelay', - 'workbench.editor.showTabs', - 'files.encoding', - 'files.autoGuessEncoding', 'git.enabled', 'http.proxyStrictSSL', - 'terminal.integrated.fontFamily', - 'workbench.editor.enablePreviewFromQuickOpen', - 'workbench.editor.swipeToNavigate', + 'javascript.validate.enable', 'php.builtInCompletions.enable', 'php.validate.enable', 'php.validate.run', - 'workbench.welcome.enabled', + 'terminal.integrated.fontFamily', + 'window.openFilesInNewWindow', + 'window.restoreWindows', + 'window.zoomLevel', + 'workbench.editor.enablePreview', + 'workbench.editor.enablePreviewFromQuickOpen', + 'workbench.editor.showTabs', + 'workbench.editor.swipeToNavigate', + 'workbench.sideBar.location', 'workbench.startupEditor', + 'workbench.statusBar.visible', + 'workbench.welcome.enabled', ]; export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable { diff --git a/src/vs/platform/telemetry/node/appInsightsAppender.ts b/src/vs/platform/telemetry/node/appInsightsAppender.ts index 898f5bfc02c..e8a7ead4570 100644 --- a/src/vs/platform/telemetry/node/appInsightsAppender.ts +++ b/src/vs/platform/telemetry/node/appInsightsAppender.ts @@ -9,6 +9,7 @@ import { isObject } from 'vs/base/common/types'; import { safeStringify, mixin } from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ILogService } from 'vs/platform/log/common/log'; let _initialized = false; @@ -53,7 +54,8 @@ export class AppInsightsAppender implements ITelemetryAppender { constructor( private _eventPrefix: string, private _defaultData: { [key: string]: any }, - aiKeyOrClientFactory: string | (() => typeof appInsights.client) // allow factory function for testing + aiKeyOrClientFactory: string | (() => typeof appInsights.client), // allow factory function for testing + @ILogService private _logService?: ILogService ) { if (!this._defaultData) { this._defaultData = Object.create(null); @@ -133,8 +135,12 @@ export class AppInsightsAppender implements ITelemetryAppender { return; } data = mixin(data, this._defaultData); - let { properties, measurements } = AppInsightsAppender._getData(data); - this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, properties, measurements); + data = AppInsightsAppender._getData(data); + + if (this._logService) { + this._logService.trace(`telemetry/${eventName}`, data); + } + this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, data.properties, data.measurements); } dispose(): TPromise { diff --git a/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts b/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts index a298f26aa91..040a51185f9 100644 --- a/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { ILogService, AbstractLogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; interface IAppInsightsEvent { eventName: string; @@ -39,11 +40,61 @@ class AppInsightsMock { } } +class TestableLogService extends AbstractLogService implements ILogService { + _serviceBrand: any; + + public logs: string[] = []; + + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { + super(); + this.setLevel(logLevel); + } + + trace(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Trace) { + this.logs.push(message + JSON.stringify(args)); + } + } + + debug(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Debug) { + this.logs.push(message); + } + } + + info(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Info) { + this.logs.push(message); + } + } + + warn(message: string | Error, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Warning) { + this.logs.push(message.toString()); + } + } + + error(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Error) { + this.logs.push(message); + } + } + + critical(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Critical) { + this.logs.push(message); + } + } + + dispose(): void { } +} + suite('AIAdapter', () => { var appInsightsMock: AppInsightsMock; var adapter: AppInsightsAppender; var prefix = 'prefix'; + setup(() => { appInsightsMock = new AppInsightsMock(); adapter = new AppInsightsAppender(prefix, undefined, () => appInsightsMock); @@ -141,4 +192,27 @@ suite('AIAdapter', () => { assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({ 'testProperty': 'test' })); assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'], 1); }); + + test('Do not Log Telemetry if log level is not trace', () => { + const logService = new TestableLogService(LogLevel.Info); + adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService); + adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); + assert.equal(logService.logs.length, 0); + }); + + test('Log Telemetry if log level is trace', () => { + const logService = new TestableLogService(LogLevel.Trace); + adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService); + adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); + assert.equal(logService.logs.length, 1); + assert.equal(logService.logs[0], 'telemetry/testEvent' + JSON.stringify([{ + properties: { + hello: 'world', + 'common.platform': 'Windows' + }, + measurements: { + isTrue: 1, numberBetween1And3: 2 + } + }])); + }); }); \ No newline at end of file diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 09c79f74f7c..20da2b69538 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -177,7 +177,7 @@ export const inputBackground = registerColor('input.background', { dark: '#3C3C3 export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground.")); export const inputBorder = registerColor('input.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('inputBoxBorder', "Input box border.")); export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC', light: '#007ACC', hc: activeContrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); -export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { dark: null, light: null, hc: null }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); +export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hc: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); @@ -197,7 +197,7 @@ export const listActiveSelectionBackground = registerColor('list.activeSelection export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#CCCEDB', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: listFocusBackground, light: listFocusBackground, hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); @@ -223,6 +223,11 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); +export const breadcrumbsForeground = registerColor('breadcrumb.breadcrumbsForeground', { light: Color.fromHex('#6C6C6C').transparent(.7), dark: Color.fromHex('#CCCCCC').transparent(.7), hc: Color.white.transparent(.7) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsFocusForeground = registerColor('breadcrumb.breadcrumbsFocusForeground', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.breadcrumbsActiveSelectionForeground', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items.")); +export const breadcrumbsPickerBackground = registerColor('breadcrumb.breadcrumbsPickerBackground', { light: '#ECECEC', dark: '#252526', hc: Color.black }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); + /** * Editor background color. * Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254 @@ -287,6 +292,8 @@ export const diffRemoved = registerColor('diffEditor.removedTextBackground', { d export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', { dark: null, light: null, hc: '#33ff2eff' }, nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.')); export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hc: '#FF008F' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.')); +export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.')); + /** * Merge-conflict colors */ diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index d5627901f0f..ff6542c0a9a 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -6,7 +6,7 @@ 'use strict'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -261,4 +261,24 @@ 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; + breadcrumbsForeground?: ColorIdentifier; + breadcrumbsHoverForeground?: ColorIdentifier; + breadcrumbsFocusForeground?: ColorIdentifier; + breadcrumbsFocusAndSelectionForeground?: ColorIdentifier; +} + +export const defaultBreadcrumbsStyles = { + breadcrumbsBackground: editorBackground, + breadcrumbsForeground: breadcrumbsForeground, + breadcrumbsHoverForeground: breadcrumbsFocusForeground, + breadcrumbsFocusForeground: breadcrumbsFocusForeground, + breadcrumbsFocusAndSelectionForeground: breadcrumbsActiveSelectionForeground, +}; + +export function attachBreadcrumbsStyler(widget: IThemable, themeService: IThemeService, style?: IBreadcrumbsWidgetStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultBreadcrumbsStyles, ...style }, widget); +} diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index c58b79603bc..7c1e2f9bae6 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Color } from 'vs/base/common/color'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/platform/registry/common/platform'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Event, Emitter } from 'vs/base/common/event'; @@ -111,12 +111,10 @@ class ThemingRegistry implements IThemingRegistry { public onThemeChange(participant: IThemingParticipant): IDisposable { this.themingParticipants.push(participant); this.onThemingParticipantAddedEmitter.fire(participant); - return { - dispose: () => { - const idx = this.themingParticipants.indexOf(participant); - this.themingParticipants.splice(idx, 1); - } - }; + return toDisposable(() => { + const idx = this.themingParticipants.indexOf(participant); + this.themingParticipants.splice(idx, 1); + }); } public get onThemingParticipantAdded(): Event { diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 8a1b2dbbf7c..5f187e283ae 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -48,8 +48,13 @@ export enum StateType { Ready = 'ready', } +export enum UpdateType { + Setup, + Archive +} + export type Uninitialized = { type: StateType.Uninitialized }; -export type Idle = { type: StateType.Idle }; +export type Idle = { type: StateType.Idle, updateType: UpdateType }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates, context: any }; export type AvailableForDownload = { type: StateType.AvailableForDownload, update: IUpdate }; export type Downloading = { type: StateType.Downloading, update: IUpdate }; @@ -61,7 +66,7 @@ export type State = Uninitialized | Idle | CheckingForUpdates | AvailableForDown export const State = { Uninitialized: { type: StateType.Uninitialized } as Uninitialized, - Idle: { type: StateType.Idle } as Idle, + Idle: (updateType: UpdateType) => ({ type: StateType.Idle, updateType }) as Idle, CheckingForUpdates: (context: any) => ({ type: StateType.CheckingForUpdates, context } as CheckingForUpdates), AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 245df1d3053..b689dd6bd67 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -11,7 +11,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import product from 'vs/platform/node/product'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IUpdateService, State, StateType, AvailableForDownload } from 'vs/platform/update/common/update'; +import { IUpdateService, State, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; import { IRequestService } from 'vs/platform/request/node/request'; @@ -72,7 +72,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - this.setState({ type: StateType.Idle }); + this.setState(State.Idle(this.getUpdateType())); // Start checking for updates after 30 seconds this.scheduleCheckForUpdates(30 * 1000) @@ -173,6 +173,10 @@ export abstract class AbstractUpdateService implements IUpdateService { }); } + protected getUpdateType(): UpdateType { + return UpdateType.Archive; + } + protected doQuitAndInstall(): void { // noop } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 81b935cf758..01d80b45ea1 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -11,7 +11,7 @@ import { Event, fromNodeEventEmitter } from 'vs/base/common/event'; import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { State, IUpdate, StateType, UpdateType } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -46,7 +46,7 @@ export class DarwinUpdateService extends AbstractUpdateService { private onError(err: string): void { this.logService.error('UpdateService error: ', err); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); } protected buildUpdateFeedUrl(quality: string): string | undefined { @@ -101,7 +101,7 @@ export class DarwinUpdateService extends AbstractUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!this.state.context }); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); } protected doQuitAndInstall(): void { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 126e7d8ade2..87c60c5d436 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -9,7 +9,7 @@ import product from 'vs/platform/node/product'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IRequestService } from 'vs/platform/request/node/request'; -import { State, IUpdate, AvailableForDownload } from 'vs/platform/update/common/update'; +import { State, IUpdate, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -55,7 +55,7 @@ export class LinuxUpdateService extends AbstractUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); } else { this.setState(State.AvailableForDownload(update)); } @@ -69,7 +69,7 @@ export class LinuxUpdateService extends AbstractUpdateService { } */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); }); } @@ -82,7 +82,7 @@ export class LinuxUpdateService extends AbstractUpdateService { shell.openExternal(state.update.url); } - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); return TPromise.as(null); } } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index baf3a154061..6f5e750aafa 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -5,7 +5,7 @@ 'use strict'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; import { memoize } from 'vs/base/common/decorators'; @@ -14,7 +14,7 @@ import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycle import { IRequestService } from 'vs/platform/request/node/request'; import product from 'vs/platform/node/product'; import { TPromise, Promise } from 'vs/base/common/winjs.base'; -import { State, IUpdate, StateType, AvailableForDownload } from 'vs/platform/update/common/update'; +import { State, IUpdate, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -44,17 +44,12 @@ interface IAvailableUpdate { updateFilePath?: string; } -enum UpdateType { - Automatic, - Manual -} - let _updateType: UpdateType | undefined = undefined; function getUpdateType(): UpdateType { if (typeof _updateType === 'undefined') { _updateType = fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe')) - ? UpdateType.Automatic - : UpdateType.Manual; + ? UpdateType.Setup + : UpdateType.Archive; } return _updateType; @@ -81,6 +76,20 @@ export class Win32UpdateService extends AbstractUpdateService { @ILogService logService: ILogService ) { super(lifecycleService, configurationService, environmentService, requestService, logService); + + if (getUpdateType() === UpdateType.Setup) { + /* __GDPR__ + "update:win32SetupTarget" : { + "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + /* __GDPR__ + "update:winSetupTarget" : { + "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + telemetryService.publicLog('update:win32SetupTarget', { target: product.target }); + } } protected buildUpdateFeedUrl(quality: string): string | undefined { @@ -90,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService { platform += '-x64'; } - if (getUpdateType() === UpdateType.Manual) { + if (getUpdateType() === UpdateType.Archive) { platform += '-archive'; } else if (product.target === 'user') { platform += '-user'; @@ -109,6 +118,8 @@ export class Win32UpdateService extends AbstractUpdateService { this.requestService.request({ url: this.url }) .then(asJson) .then(update => { + const updateType = getUpdateType(); + if (!update || !update.url || !update.version || !update.productVersion) { /* __GDPR__ "update:notAvailable" : { @@ -117,11 +128,11 @@ export class Win32UpdateService extends AbstractUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(updateType)); return TPromise.as(null); } - if (getUpdateType() === UpdateType.Manual) { + if (updateType === UpdateType.Archive) { this.setState(State.AvailableForDownload(update)); return TPromise.as(null); } @@ -170,13 +181,13 @@ export class Win32UpdateService extends AbstractUpdateService { } */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(getUpdateType())); }); } protected doDownloadUpdate(state: AvailableForDownload): TPromise { shell.openExternal(state.update.url); - this.setState(State.Idle); + this.setState(State.Idle(getUpdateType())); return TPromise.as(null); } @@ -220,7 +231,7 @@ export class Win32UpdateService extends AbstractUpdateService { child.once('exit', () => { this.availableUpdate = undefined; - this.setState(State.Idle); + this.setState(State.Idle(getUpdateType())); }); const readyMutexName = `${product.win32MutexName}-ready`; @@ -249,4 +260,8 @@ export class Win32UpdateService extends AbstractUpdateService { }); } } + + protected getUpdateType(): UpdateType { + return getUpdateType(); + } } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 9c94e242e06..269f0bf03d0 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -11,12 +11,13 @@ import { Event, latch, anyEvent } from 'vs/base/common/event'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { PerformanceEntry } from 'vs/base/common/performance'; import { LogLevel } from 'vs/platform/log/common/log'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import URI, { UriComponents } from 'vs/base/common/uri'; export const IWindowsService = createDecorator('windowsService'); @@ -120,12 +121,13 @@ export interface IWindowsService { openDevTools(windowId: number, options?: IDevToolsOptions): TPromise; toggleDevTools(windowId: number): TPromise; closeWorkspace(windowId: number): TPromise; + enterWorkspace(windowId: number, path: string): TPromise; createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise; saveAndEnterWorkspace(windowId: number, path: string): TPromise; toggleFullScreen(windowId: number): TPromise; setRepresentedFilename(windowId: number, fileName: string): TPromise; addRecentlyOpened(files: string[]): TPromise; - removeFromRecentlyOpened(paths: string[]): TPromise; + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): TPromise; clearRecentlyOpened(): TPromise; getRecentlyOpened(windowId: number): TPromise; focusWindow(windowId: number): TPromise; @@ -155,13 +157,14 @@ export interface IWindowsService { toggleSharedProcess(): TPromise; // Global methods - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise; + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise; openNewWindow(): TPromise; showWindow(windowId: number): TPromise; - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]>; + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>; getWindowCount(): TPromise; log(severity: string, ...messages: string[]): TPromise; showItemInFolder(path: string): TPromise; + getActiveWindowId(): TPromise; // This needs to be handled from browser process to prevent // foreground ordering issues on Windows @@ -199,6 +202,7 @@ export interface IWindowService { toggleDevTools(): TPromise; closeWorkspace(): TPromise; updateTouchBar(items: ISerializableCommandAction[][]): TPromise; + enterWorkspace(path: string): TPromise; createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise; saveAndEnterWorkspace(path: string): TPromise; toggleFullScreen(): TPromise; @@ -206,7 +210,7 @@ export interface IWindowService { getRecentlyOpened(): TPromise; focusWindow(): TPromise; closeWindow(): TPromise; - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise; + openWindow(paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise; isFocused(): TPromise; setDocumentEdited(flag: boolean): TPromise; isMaximized(): TPromise; @@ -314,7 +318,7 @@ export interface IOpenFileRequest { } export interface IAddFoldersRequest { - foldersToAdd: IPath[]; + foldersToAdd: UriComponents[]; } export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { @@ -332,7 +336,7 @@ export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { backupPath?: string; workspace?: IWorkspaceIdentifier; - folderPath?: string; + folderUri?: ISingleFolderWorkspaceIdentifier; zoomLevel?: number; fullscreen?: boolean; @@ -357,19 +361,31 @@ export interface IRunActionInWindowRequest { export class ActiveWindowManager implements IDisposable { private disposables: IDisposable[] = []; - private _activeWindowId: number; + private firstActiveWindowIdPromise: TPromise | null; + private _activeWindowId: number | undefined; constructor(@IWindowsService windowsService: IWindowsService) { const onActiveWindowChange = latch(anyEvent(windowsService.onWindowOpen, windowsService.onWindowFocus)); onActiveWindowChange(this.setActiveWindow, this, this.disposables); + + this.firstActiveWindowIdPromise = windowsService.getActiveWindowId() + .then(id => (typeof this._activeWindowId === 'undefined') && this.setActiveWindow(id)); } private setActiveWindow(windowId: number) { + if (this.firstActiveWindowIdPromise) { + this.firstActiveWindowIdPromise = null; + } + this._activeWindowId = windowId; } - get activeClientId(): string { - return `window:${this._activeWindowId}`; + getActiveClientId(): TPromise { + if (this.firstActiveWindowIdPromise) { + return this.firstActiveWindowIdPromise; + } + + return TPromise.as(`window:${this._activeWindowId}`); } dispose() { diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index 0adca2f73b4..7398b588718 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event, buffer } from 'vs/base/common/event'; 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 { IWorkspaceIdentifier, IWorkspaceFolderCreationData, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import URI from 'vs/base/common/uri'; @@ -32,21 +32,23 @@ export interface IWindowsChannel extends IChannel { call(command: 'showSaveDialog', arg: [number, SaveDialogOptions]): TPromise; call(command: 'showOpenDialog', arg: [number, OpenDialogOptions]): TPromise; call(command: 'reloadWindow', arg: [number, ParsedArgs]): TPromise; + call(command: 'openDevTools', arg: [number, IDevToolsOptions]): TPromise; call(command: 'toggleDevTools', arg: number): TPromise; call(command: 'closeWorkspace', arg: number): TPromise; + call(command: 'enterWorkspace', arg: [number, string]): TPromise; call(command: 'createAndEnterWorkspace', arg: [number, IWorkspaceFolderCreationData[], string]): TPromise; call(command: 'saveAndEnterWorkspace', arg: [number, string]): TPromise; call(command: 'toggleFullScreen', arg: number): TPromise; call(command: 'setRepresentedFilename', arg: [number, string]): TPromise; call(command: 'addRecentlyOpened', arg: string[]): TPromise; - call(command: 'removeFromRecentlyOpened', arg: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise; + call(command: 'removeFromRecentlyOpened', arg: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): TPromise; call(command: 'clearRecentlyOpened'): TPromise; call(command: 'getRecentlyOpened', arg: number): TPromise; - call(command: 'showPreviousWindowTab', arg: number): TPromise; - call(command: 'showNextWindowTab', arg: number): TPromise; - call(command: 'moveWindowTabToNewWindow', arg: number): TPromise; - call(command: 'mergeAllWindowTabs', arg: number): TPromise; - call(command: 'toggleWindowTabsBar', arg: number): TPromise; + call(command: 'showPreviousWindowTab'): TPromise; + call(command: 'showNextWindowTab'): TPromise; + call(command: 'moveWindowTabToNewWindow'): TPromise; + call(command: 'mergeAllWindowTabs'): TPromise; + call(command: 'toggleWindowTabsBar'): TPromise; call(command: 'updateTouchBar', arg: [number, ISerializableCommandAction[][]]): TPromise; call(command: 'focusWindow', arg: number): TPromise; call(command: 'closeWindow', arg: number): TPromise; @@ -58,21 +60,21 @@ export interface IWindowsChannel extends IChannel { call(command: 'onWindowTitleDoubleClick', arg: number): TPromise; call(command: 'setDocumentEdited', arg: [number, boolean]): TPromise; call(command: 'quit'): TPromise; - call(command: 'openWindow', arg: [number, string[], { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }]): TPromise; + call(command: 'openWindow', arg: [number, URI[], { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }]): TPromise; call(command: 'openNewWindow'): TPromise; call(command: 'showWindow', arg: number): TPromise; - call(command: 'getWindows'): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]>; + call(command: 'getWindows'): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>; call(command: 'getWindowCount'): TPromise; - call(command: 'relaunch', arg: { addArgs?: string[], removeArgs?: string[] }): TPromise; + call(command: 'relaunch', arg: [{ addArgs?: string[], removeArgs?: string[] }]): TPromise; call(command: 'whenSharedProcessReady'): TPromise; call(command: 'toggleSharedProcess'): TPromise; call(command: 'log', arg: [string, string[]]): TPromise; call(command: 'showItemInFolder', arg: string): TPromise; + call(command: 'getActiveWindowId'): TPromise; call(command: 'openExternal', arg: string): TPromise; call(command: 'startCrashReporter', arg: CrashReporterStartOptions): TPromise; call(command: 'openAccessibilityOptions'): TPromise; call(command: 'openAboutDialog'): TPromise; - call(command: string, arg?: any): TPromise; } export class WindowsChannel implements IWindowsChannel { @@ -119,6 +121,7 @@ export class WindowsChannel implements IWindowsChannel { case 'openDevTools': return this.service.openDevTools(arg[0], arg[1]); case 'toggleDevTools': return this.service.toggleDevTools(arg); case 'closeWorkspace': return this.service.closeWorkspace(arg); + case 'enterWorkspace': return this.service.enterWorkspace(arg[0], arg[1]); case 'createAndEnterWorkspace': { const rawFolders: IWorkspaceFolderCreationData[] = arg[1]; let folders: IWorkspaceFolderCreationData[]; @@ -137,7 +140,7 @@ export class WindowsChannel implements IWindowsChannel { case 'toggleFullScreen': return this.service.toggleFullScreen(arg); case 'setRepresentedFilename': return this.service.setRepresentedFilename(arg[0], arg[1]); case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg); - case 'removeFromRecentlyOpened': return this.service.removeFromRecentlyOpened(arg); + case 'removeFromRecentlyOpened': return this.service.removeFromRecentlyOpened(isSingleFolderWorkspaceIdentifier(arg) ? URI.revive(arg) : arg); case 'clearRecentlyOpened': return this.service.clearRecentlyOpened(); case 'showPreviousWindowTab': return this.service.showPreviousWindowTab(); case 'showNextWindowTab': return this.service.showNextWindowTab(); @@ -155,7 +158,7 @@ export class WindowsChannel implements IWindowsChannel { case 'minimizeWindow': return this.service.minimizeWindow(arg); case 'onWindowTitleDoubleClick': return this.service.onWindowTitleDoubleClick(arg); case 'setDocumentEdited': return this.service.setDocumentEdited(arg[0], arg[1]); - case 'openWindow': return this.service.openWindow(arg[0], arg[1], arg[2]); + case 'openWindow': return this.service.openWindow(arg[0], arg[1] ? (arg[1]).map(r => URI.revive(r)) : arg[1], arg[2]); case 'openNewWindow': return this.service.openNewWindow(); case 'showWindow': return this.service.showWindow(arg); case 'getWindows': return this.service.getWindows(); @@ -166,6 +169,7 @@ export class WindowsChannel implements IWindowsChannel { case 'quit': return this.service.quit(); case 'log': return this.service.log(arg[0], arg[1]); case 'showItemInFolder': return this.service.showItemInFolder(arg); + case 'getActiveWindowId': return this.service.getActiveWindowId(); case 'openExternal': return this.service.openExternal(arg); case 'startCrashReporter': return this.service.startCrashReporter(arg); case 'openAccessibilityOptions': return this.service.openAccessibilityOptions(); @@ -232,6 +236,10 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('closeWorkspace', windowId); } + enterWorkspace(windowId: number, path: string): TPromise { + return this.channel.call('enterWorkspace', [windowId, path]); + } + createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.channel.call('createAndEnterWorkspace', [windowId, folders, path]); } @@ -252,7 +260,7 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('addRecentlyOpened', files); } - removeFromRecentlyOpened(paths: string[]): TPromise { + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): TPromise { return this.channel.call('removeFromRecentlyOpened', paths); } @@ -261,7 +269,11 @@ export class WindowsChannelClient implements IWindowsService { } getRecentlyOpened(windowId: number): TPromise { - return this.channel.call('getRecentlyOpened', windowId); + return this.channel.call('getRecentlyOpened', windowId) + .then(recentlyOpened => { + recentlyOpened.workspaces = recentlyOpened.workspaces.map(workspace => isWorkspaceIdentifier(workspace) ? workspace : URI.revive(workspace)); + return recentlyOpened; + }); } showPreviousWindowTab(): TPromise { @@ -336,7 +348,7 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('toggleSharedProcess'); } - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise { return this.channel.call('openWindow', [windowId, paths, options]); } @@ -348,8 +360,8 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('showWindow', windowId); } - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { - return this.channel.call('getWindows'); + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { + return this.channel.call('getWindows').then(result => { result.forEach(win => win.folderUri = win.folderUri ? URI.revive(win.folderUri) : win.folderUri); return result; }); } getWindowCount(): TPromise { @@ -364,6 +376,10 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('showItemInFolder', path); } + getActiveWindowId(): TPromise { + return this.channel.call('getActiveWindowId'); + } + openExternal(url: string): TPromise { return this.channel.call('openExternal', url); } diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 0a8e9c01f93..c32c299263e 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -12,6 +12,7 @@ import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; +import URI from 'vs/base/common/uri'; export class WindowService implements IWindowService { @@ -81,6 +82,10 @@ export class WindowService implements IWindowService { return this.windowsService.closeWorkspace(this.windowId); } + enterWorkspace(path: string): TPromise { + return this.windowsService.enterWorkspace(this.windowId, path); + } + createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.windowsService.createAndEnterWorkspace(this.windowId, folders, path); } @@ -89,7 +94,7 @@ export class WindowService implements IWindowService { return this.windowsService.saveAndEnterWorkspace(this.windowId, path); } - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise { + openWindow(paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise { return this.windowsService.openWindow(this.windowId, paths, options); } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 320f8387935..1c38b451768 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -13,6 +13,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IProcessEnvironment } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import URI from 'vs/base/common/uri'; export interface IWindowState { width?: number; @@ -35,7 +36,7 @@ export interface ICodeWindow { win: Electron.BrowserWindow; config: IWindowConfiguration; - openedFolderPath: string; + openedFolderUri: URI; openedWorkspace: IWorkspaceIdentifier; backupPath: string; @@ -92,6 +93,7 @@ export interface IWindowsMainService { // methods ready(initialUserEnv: IProcessEnvironment): void; reload(win: ICodeWindow, cli?: ParsedArgs): void; + enterWorkspace(win: ICodeWindow, path: string): TPromise; createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise; saveAndEnterWorkspace(win: ICodeWindow, path: string): TPromise; closeWorkspace(win: ICodeWindow): void; @@ -122,7 +124,7 @@ export interface IOpenConfiguration { contextWindowId?: number; cli: ParsedArgs; userEnv?: IProcessEnvironment; - pathsToOpen?: string[]; + urisToOpen?: URI[]; preferNewWindow?: boolean; forceNewWindow?: boolean; forceReuseWindow?: boolean; diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 2987443c204..0e2ace3e04f 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -14,12 +14,12 @@ import product from 'vs/platform/node/product'; import { IWindowsService, OpenContext, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IDevToolsOptions } from 'vs/platform/windows/common/windows'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { shell, crashReporter, app, Menu, clipboard, BrowserWindow } from 'electron'; -import { Event, fromNodeEventEmitter, mapEvent, filterEvent, anyEvent } from 'vs/base/common/event'; +import { Event, fromNodeEventEmitter, mapEvent, filterEvent, anyEvent, latch } from 'vs/base/common/event'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IWindowsMainService, ISharedProcess } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { Schemas } from 'vs/base/common/network'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -32,6 +32,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable private disposables: IDisposable[] = []; + private _activeWindowId: number | undefined; + readonly onWindowOpen: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-created', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowFocus: Event = anyEvent( mapEvent(filterEvent(mapEvent(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), w => !!w), w => w.id), @@ -54,6 +56,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable @ILogService private logService: ILogService ) { urlService.registerHandler(this); + + // remember last active window id + latch(anyEvent(this.onWindowOpen, this.onWindowFocus)) + (id => this._activeWindowId = id, null, this.disposables); } pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { @@ -165,6 +171,17 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + enterWorkspace(windowId: number, path: string): TPromise { + this.logService.trace('windowsService#enterWorkspace', windowId); + const codeWindow = this.windowsMainService.getWindowById(windowId); + + if (codeWindow) { + return this.windowsMainService.enterWorkspace(codeWindow, path); + } + + return TPromise.as(null); + } + createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { this.logService.trace('windowsService#createAndEnterWorkspace', windowId); const codeWindow = this.windowsMainService.getWindowById(windowId); @@ -216,7 +233,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - removeFromRecentlyOpened(paths: string[]): TPromise { + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): TPromise { this.logService.trace('windowsService#removeFromRecentlyOpened'); this.historyService.removeFromRecentlyOpened(paths); @@ -235,7 +252,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable const codeWindow = this.windowsMainService.getWindowById(windowId); if (codeWindow) { - return TPromise.as(this.historyService.getRecentlyOpened(codeWindow.config.workspace || codeWindow.config.folderPath, codeWindow.config.filesToOpen)); + return TPromise.as(this.historyService.getRecentlyOpened(codeWindow.config.workspace || codeWindow.config.folderUri, codeWindow.config.filesToOpen)); } return TPromise.as(this.historyService.getRecentlyOpened()); @@ -375,7 +392,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise { this.logService.trace('windowsService#openWindow'); if (!paths || !paths.length) { return TPromise.as(null); @@ -384,8 +401,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable this.windowsMainService.open({ context: OpenContext.API, contextWindowId: windowId, - cli: this.environmentService.args, - pathsToOpen: paths, + urisToOpen: paths, + cli: options && options.args ? { ...this.environmentService.args, ...options.args } : this.environmentService.args, forceNewWindow: options && options.forceNewWindow, forceReuseWindow: options && options.forceReuseWindow, forceOpenWorkspaceAsFile: options && options.forceOpenWorkspaceAsFile @@ -411,10 +428,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { this.logService.trace('windowsService#getWindows'); const windows = this.windowsMainService.getWindows(); - const result = windows.map(w => ({ id: w.id, workspace: w.openedWorkspace, openedFolderPath: w.openedFolderPath, title: w.win.getTitle(), filename: w.getRepresentedFilename() })); + const result = windows.map(w => ({ id: w.id, workspace: w.openedWorkspace, folderUri: w.openedFolderUri, title: w.win.getTitle(), filename: w.getRepresentedFilename() })); return TPromise.as(result); } @@ -435,6 +452,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + getActiveWindowId(): TPromise { + return TPromise.as(this._activeWindowId); + } + openExternal(url: string): TPromise { this.logService.trace('windowsService#openExternal'); return TPromise.as(shell.openExternal(url)); @@ -542,9 +563,9 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable private openFileForURI(uri: URI): TPromise { const cli = assign(Object.create(null), this.environmentService.args, { goto: true }); - const pathsToOpen = [uri.fsPath]; + const urisToOpen = [uri]; - this.windowsMainService.open({ context: OpenContext.API, cli, pathsToOpen }); + this.windowsMainService.open({ context: OpenContext.API, cli, urisToOpen }); return TPromise.wrap(true); } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 2263c848f17..39a413b09e4 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TernarySearchTree } from 'vs/base/common/map'; import { Event } from 'vs/base/common/event'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { isLinux } from 'vs/base/common/platform'; diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d0d52e1265b..7bfa4235a63 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -13,9 +13,10 @@ import { basename, dirname, join } from 'vs/base/common/paths'; import { isLinux } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Event } from 'vs/base/common/event'; -import { tildify, getPathLabel } from 'vs/base/common/labels'; +import { getPathLabel, getBaseLabel } from 'vs/base/common/labels'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import URI from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); export const IWorkspacesService = createDecorator('workspacesService'); @@ -27,7 +28,7 @@ export const UNTITLED_WORKSPACE_NAME = 'workspace.json'; /** * A single folder workspace identifier is just the path to the folder. */ -export type ISingleFolderWorkspaceIdentifier = string; +export type ISingleFolderWorkspaceIdentifier = URI; export interface IWorkspaceIdentifier { id: string; @@ -92,6 +93,8 @@ export interface IWorkspacesMainService extends IWorkspacesService { createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; + resolveWorkspace(path: string): TPromise; + resolveWorkspaceSync(path: string): IResolvedWorkspace; isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; @@ -113,7 +116,13 @@ export function getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFold // Workspace: Single Folder if (isSingleFolderWorkspaceIdentifier(workspace)) { - return tildify(workspace, environmentService.userHome); + // Folder on disk + if (workspace.scheme === Schemas.file) { + return options && options.verbose ? getPathLabel(workspace, environmentService) : getBaseLabel(workspace); + } + + // Remote folder + return options && options.verbose ? getPathLabel(workspace, environmentService) : `${getBaseLabel(workspace)} (${workspace.scheme})`; } // Workspace: Untitled @@ -132,7 +141,7 @@ export function getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFold } export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier { - return typeof obj === 'string'; + return obj instanceof URI; } export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 45a50ab6543..1f6480b5ad2 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -58,6 +58,14 @@ export class WorkspacesMainService implements IWorkspacesMainService { return this._onUntitledWorkspaceDeleted.event; } + resolveWorkspace(path: string): TPromise { + if (!this.isWorkspacePath(path)) { + return TPromise.as(null); // does not look like a valid workspace config file + } + + return readFile(path, 'utf8').then(contents => this.doResolveWorkspace(path, contents)); + } + resolveWorkspaceSync(path: string): IResolvedWorkspace { if (!this.isWorkspacePath(path)) { return null; // does not look like a valid workspace config file diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 7e66b3f3a7c..607f418e181 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2156,6 +2156,41 @@ declare module 'vscode' { resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult; } + /** + * Information about where a symbol is defined. + * + * Provides additional metadata over normal [location](#Location) definitions, including the range of + * the defining symbol + */ + export interface DefinitionLink { + /** + * Span of the symbol being defined in the source file. + * + * Used as the underlined span for mouse definition hover. Defaults to the word range at + * the definition position. + */ + originSelectionRange?: Range; + + /** + * The resource identifier of the definition. + */ + targetUri: Uri; + + /** + * The full range of the definition. + * + * For a class definition for example, this would be the entire body of the class definition. + */ + targetRange: Range; + + /** + * The span of the symbol definition. + * + * For a class definition, this would be the class name itself in the class definition. + */ + targetSelectionRange?: Range; + } + /** * The definition of a symbol represented as one or many [locations](#Location). * For most programming languages there is only one location at which a symbol is @@ -2179,7 +2214,7 @@ declare module 'vscode' { * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -2197,7 +2232,7 @@ declare module 'vscode' { * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -2215,7 +2250,7 @@ declare module 'vscode' { * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -4557,22 +4592,22 @@ declare module 'vscode' { /** * The clean task group; */ - public static Clean: TaskGroup; + static Clean: TaskGroup; /** * The build task group; */ - public static Build: TaskGroup; + static Build: TaskGroup; /** * The rebuild all task group; */ - public static Rebuild: TaskGroup; + static Rebuild: TaskGroup; /** * The test all task group; */ - public static Test: TaskGroup; + static Test: TaskGroup; private constructor(id: string, label: string); } @@ -5478,8 +5513,6 @@ declare module 'vscode' { /** * Editor position of the panel. This property is only set if the webview is in * one of the editor view columns. - * - * @deprecated */ readonly viewColumn?: ViewColumn; @@ -5732,6 +5765,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 @@ -6071,6 +6119,29 @@ declare module 'vscode' { */ export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; + /** + * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list + * 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 + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * + * @return A new [QuickPick](#QuickPick). + */ + export function createQuickPick(): QuickPick; + + /** + * 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 + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * + * @return A new [InputBox](#InputBox). + */ + export function createInputBox(): InputBox; + /** * Create a new [output channel](#OutputChannel) with the given name. * @@ -6203,6 +6274,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. * @@ -6512,6 +6606,264 @@ declare module 'vscode' { cancellable?: boolean; } + /** + * A light-weight user input UI that is intially not visible. After + * configuring it through its properties the extension can make it + * visible by calling [QuickInput.show](#QuickInput.show). + * + * There are several reasons why this UI might have to be hidden and + * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). + * (Examples include: an explict call to [QuickInput.hide](#QuickInput.hide), + * the user pressing Esc, some other input UI opening, etc.) + * + * A user pressing Enter or some other gesture implying acceptance + * of the current state does not automatically hide this UI component. + * It is up to the extension to decide whether to accept the user's input + * and if the UI should indeed be hidden through a call to [QuickInput.hide](#QuickInput.hide). + * + * When the extension no longer needs this input UI, it should + * [QuickInput.dispose](#QuickInput.dispose) it to allow for freeing up + * any resources associated with it. + * + * See [QuickPick](#QuickPick) and [InputBox](#InputBox) for concrete UIs. + */ + export interface QuickInput { + + /** + * An optional title. + */ + title: string | undefined; + + /** + * An optional current step count. + */ + step: number | undefined; + + /** + * An optional total step count. + */ + totalSteps: number | undefined; + + /** + * If the UI should allow for user input. Defaults to true. + * + * Change this to false, e.g., while validating user input or + * loading data for the next step in user input. + */ + enabled: boolean; + + /** + * If the UI should show a progress indicator. Defaults to false. + * + * Change this to true, e.g., while loading more data or validating + * user input. + */ + busy: boolean; + + /** + * If the UI should stay open even when loosing UI focus. Defaults to false. + */ + ignoreFocusOut: boolean; + + /** + * Makes the input UI visible in its current configuration. Any other input + * UI will first fire an [QuickInput.onDidHide](#QuickInput.onDidHide) event. + */ + show(): void; + + /** + * Hides this input UI. This will also fire an [QuickInput.onDidHide](#QuickInput.onDidHide) + * event. + */ + hide(): void; + + /** + * An event signaling when this input UI is hidden. + * + * There are several reasons why this UI might have to be hidden and + * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). + * (Examples include: an explict call to [QuickInput.hide](#QuickInput.hide), + * the user pressing Esc, some other input UI opening, etc.) + */ + onDidHide: Event; + + /** + * Dispose of this input UI and any associated resources. If it is still + * visible, it is first hidden. After this call the input UI is no longer + * functional and no additional methods or properties on it should be + * accessed. Instead a new input UI should be created. + */ + dispose(): void; + } + + /** + * A concrete [QuickInput](#QuickInput) to let the user pick an item from a + * list of items of type T. The items can be filtered through a filter text field and + * there is an option [canSelectMany](#QuickPick.canSelectMany) to allow for + * 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 + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + */ + export interface QuickPick extends QuickInput { + + /** + * Current value of the filter text. + */ + value: string; + + /** + * Optional placeholder in the filter text. + */ + placeholder: string | undefined; + + /** + * An event signaling when the value of the filter text has changed. + */ + readonly onDidChangeValue: Event; + + /** + * An event signaling when the user indicated acceptance of the selected item(s). + */ + readonly onDidAccept: Event; + + /** + * Buttons for actions in the UI. + */ + buttons: ReadonlyArray; + + /** + * An event signaling when a button was triggered. + */ + readonly onDidTriggerButton: Event; + + /** + * Items to pick from. + */ + items: ReadonlyArray; + + /** + * If multiple items can be selected at the same time. Defaults to false. + */ + canSelectMany: boolean; + + /** + * If the filter text should also be matched against the description of the items. Defaults to false. + */ + matchOnDescription: boolean; + + /** + * If the filter text should also be matched against the detail of the items. Defaults to false. + */ + matchOnDetail: boolean; + + /** + * Active items. This can be read and updated by the extension. + */ + activeItems: ReadonlyArray; + + /** + * An event signaling when the active items have changed. + */ + readonly onDidChangeActive: Event; + + /** + * Selected items. This can be read and updated by the extension. + */ + selectedItems: ReadonlyArray; + + /** + * An event signaling when the selected items have changed. + */ + readonly onDidChangeSelection: Event; + } + + /** + * 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 + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + */ + export interface InputBox extends QuickInput { + + /** + * Current input value. + */ + value: string; + + /** + * Optional placeholder in the filter text. + */ + placeholder: string | undefined; + + /** + * If the input value should be hidden. Defaults to false. + */ + password: boolean; + + /** + * An event signaling when the value has changed. + */ + readonly onDidChangeValue: Event; + + /** + * An event signaling when the user indicated acceptance of the input value. + */ + readonly onDidAccept: Event; + + /** + * Buttons for actions in the UI. + */ + buttons: ReadonlyArray; + + /** + * An event signaling when a button was triggered. + */ + readonly onDidTriggerButton: Event; + + /** + * An optional prompt text providing some ask or explanation to the user. + */ + prompt: string | undefined; + + /** + * An optional validation message indicating a problem with the current input value. + */ + validationMessage: string | undefined; + } + + /** + * Button for an action in a [QuickPick](#QuickPick) or [InputBox](#InputBox). + */ + export interface QuickInputButton { + + /** + * Icon for the button. + */ + readonly iconPath: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; + + /** + * An optional tooltip. + */ + readonly tooltip?: string | undefined; + } + + /** + * Predefined buttons for [QuickPick](#QuickPick) and [InputBox](#InputBox). + */ + export namespace QuickInputButtons { + + /** + * A back button for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * + * When a navigation 'back' button is needed this one should be used for consistency. + * It comes with a predefined icon, tooltip and location. + */ + export const Back: QuickInputButton; + } + /** * An event describing an individual change in the text of a [document](#TextDocument). */ @@ -6954,13 +7306,13 @@ declare module 'vscode' { export const onDidChangeConfiguration: Event; /** - * Register a task provider. + * ~~Register a task provider.~~ + * + * @deprecated Use the corresponding function on the `tasks` namespace instead * * @param type The task kind type this provider is registered for. * @param provider A task provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - * - * @deprecated Use the corresponding function on the `tasks` namespace instead */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a04b43ec424..59949ab60c9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -189,12 +189,45 @@ declare module 'vscode' { provideTextSearchResults?(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; } + /** + * Options that can be set on a findTextInFiles search. + */ export interface FindTextInFilesOptions { + /** + * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) + * to restrict the search results to a [workspace folder](#WorkspaceFolder). + */ include?: GlobPattern; - exclude?: GlobPattern; + + /** + * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will + * apply, when `null` no excludes will apply. + */ + exclude?: GlobPattern | null; + + /** + * The maximum number of results to search for + */ maxResults?: number; + + /** + * 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; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ encoding?: string; } @@ -210,6 +243,24 @@ declare module 'vscode' { */ export function registerSearchProvider(scheme: string, provider: SearchProvider): Disposable; + + /** + * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ + export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; + + /** + * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param options An optional set of query options. Include and exclude patterns, maxResults, etc. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; } @@ -638,22 +689,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 { @@ -662,288 +697,6 @@ declare module 'vscode' { //#endregion - //#region QuickInput API - - export namespace window { - - /** - * A back button for [QuickPick](#QuickPick) and [InputBox](#InputBox). - * - * When a navigation 'back' button is needed this one should be used for consistency. - * It comes with a predefined icon, tooltip and location. - */ - export const quickInputBackButton: QuickInputButton; - - /** - * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list - * 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, - * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. - * - * @return A new [QuickPick](#QuickPick). - */ - export function createQuickPick(): QuickPick; - - /** - * 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, - * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. - * - * @return A new [InputBox](#InputBox). - */ - export function createInputBox(): InputBox; - } - - /** - * A light-weight user input UI that is intially not visible. After - * configuring it through its properties the extension can make it - * visible by calling [QuickInput.show](#QuickInput.show). - * - * There are several reasons why this UI might have to be hidden and - * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explict call to [QuickInput.hide](#QuickInput.hide), - * the user pressing Esc, some other input UI opening, etc.) - * - * A user pressing Enter or some other gesture implying acceptance - * of the current state does not automatically hide this UI component. - * It is up to the extension to decide whether to accept the user's input - * and if the UI should indeed be hidden through a call to [QuickInput.hide](#QuickInput.hide). - * - * When the extension no longer needs this input UI, it should - * [QuickInput.dispose](#QuickInput.dispose) it to allow for freeing up - * any resources associated with it. - * - * See [QuickPick](#QuickPick) and [InputBox](#InputBox) for concrete UIs. - */ - export interface QuickInput { - - /** - * An optional title. - */ - title: string | undefined; - - /** - * An optional current step count. - */ - step: number | undefined; - - /** - * An optional total step count. - */ - totalSteps: number | undefined; - - /** - * If the UI should allow for user input. Defaults to true. - * - * Change this to false, e.g., while validating user input or - * loading data for the next step in user input. - */ - enabled: boolean; - - /** - * If the UI should show a progress indicator. Defaults to false. - * - * Change this to true, e.g., while loading more data or validating - * user input. - */ - busy: boolean; - - /** - * If the UI should stay open even when loosing UI focus. Defaults to false. - */ - ignoreFocusOut: boolean; - - /** - * Makes the input UI visible in its current configuration. Any other input - * UI will first fire an [QuickInput.onDidHide](#QuickInput.onDidHide) event. - */ - show(): void; - - /** - * Hides this input UI. This will also fire an [QuickInput.onDidHide](#QuickInput.onDidHide) - * event. - */ - hide(): void; - - /** - * An event signaling when this input UI is hidden. - * - * There are several reasons why this UI might have to be hidden and - * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explict call to [QuickInput.hide](#QuickInput.hide), - * the user pressing Esc, some other input UI opening, etc.) - */ - onDidHide: Event; - - /** - * Dispose of this input UI and any associated resources. If it is still - * visible, it is first hidden. After this call the input UI is no longer - * functional and no additional methods or properties on it should be - * accessed. Instead a new input UI should be created. - */ - dispose(): void; - } - - /** - * A concrete [QuickInput](#QuickInput) to let the user pick an item from a - * list of items of type T. The items can be filtered through a filter text field and - * there is an option [canSelectMany](#QuickPick.canSelectMany) to allow for - * 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, - * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. - */ - export interface QuickPick extends QuickInput { - - /** - * Current value of the filter text. - */ - value: string; - - /** - * Optional placeholder in the filter text. - */ - placeholder: string | undefined; - - /** - * An event signaling when the value of the filter text has changed. - */ - readonly onDidChangeValue: Event; - - /** - * An event signaling when the user indicated acceptance of the selected item(s). - */ - readonly onDidAccept: Event; - - /** - * Buttons for actions in the UI. - */ - buttons: ReadonlyArray; - - /** - * An event signaling when a button was triggered. - */ - readonly onDidTriggerButton: Event; - - /** - * Items to pick from. - */ - items: ReadonlyArray; - - /** - * If multiple items can be selected at the same time. Defaults to false. - */ - canSelectMany: boolean; - - /** - * If the filter text should also be matched against the description of the items. Defaults to false. - */ - matchOnDescription: boolean; - - /** - * If the filter text should also be matched against the detail of the items. Defaults to false. - */ - matchOnDetail: boolean; - - /** - * Active items. This can be read and updated by the extension. - */ - activeItems: ReadonlyArray; - - /** - * An event signaling when the active items have changed. - */ - readonly onDidChangeActive: Event; - - /** - * Selected items. This can be read and updated by the extension. - */ - selectedItems: ReadonlyArray; - - /** - * An event signaling when the selected items have changed. - */ - readonly onDidChangeSelection: Event; - } - - /** - * 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, - * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. - */ - export interface InputBox extends QuickInput { - - /** - * Current input value. - */ - value: string; - - /** - * Optional placeholder in the filter text. - */ - placeholder: string | undefined; - - /** - * If the input value should be hidden. Defaults to false. - */ - password: boolean; - - /** - * An event signaling when the value has changed. - */ - readonly onDidChangeValue: Event; - - /** - * An event signaling when the user indicated acceptance of the input value. - */ - readonly onDidAccept: Event; - - /** - * Buttons for actions in the UI. - */ - buttons: ReadonlyArray; - - /** - * An event signaling when a button was triggered. - */ - readonly onDidTriggerButton: Event; - - /** - * An optional prompt text providing some ask or explanation to the user. - */ - prompt: string | undefined; - - /** - * An optional validation message indicating a problem with the current input value. - */ - validationMessage: string | undefined; - } - - /** - * Button for an action in a [QuickPick](#QuickPick) or [InputBox](#InputBox). - */ - export interface QuickInputButton { - - /** - * Icon for the button. - */ - readonly iconPath: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; - - /** - * An optional tooltip. - */ - readonly tooltip?: string | undefined; - } - - //#endregion - //#region joh: https://github.com/Microsoft/vscode/issues/10659 /** @@ -955,6 +708,11 @@ declare module 'vscode' { */ export interface WorkspaceEdit { + /** + * The number of affected resources of textual or resource changes. + */ + readonly size: number; + /** * Create a regular file. * @@ -968,7 +726,7 @@ declare module 'vscode' { * * @param uri The uri of the file that is to be deleted. */ - deleteFile(uri: Uri, options?: { recursive?: boolean }): void; + deleteFile(uri: Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean }): void; /** * Rename a file or folder. @@ -977,11 +735,7 @@ declare module 'vscode' { * @param newUri The new location. * @param options Defines if existing files should be overwritten. */ - renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean }): void; - - // replaceText(uri: Uri, range: Range, newText: string): void; - // insertText(uri: Uri, position: Position, newText: string): void; - // deleteText(uri: Uri, range: Range): void; + renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }): void; } export namespace workspace { @@ -1018,47 +772,4 @@ declare module 'vscode' { export const onDidRenameFile: Event; } //#endregion - - //#region Matt: Deinition range - - /** - * Information about where a symbol is defined. - * - * Provides additional metadata over normal [location](#Location) definitions, including the range of - * the defining symbol - */ - export interface DefinitionLink { - /** - * Span of the symbol being defined in the source file. - * - * Used as the underlined span for mouse definition hover. Defaults to the word range at - * the definition position. - */ - origin?: Range; - - /** - * The resource identifier of the definition. - */ - uri: Uri; - - /** - * The full range of the definition. - * - * For a class definition for example, this would be the entire body of the class definition. - */ - range: Range; - - /** - * The span of the symbol definition. - * - * For a class definition, this would be the class name itself in the class definition. - */ - selectionRange?: Range; - } - - export interface DefinitionProvider { - provideDefinition2?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; - } - - //#endregion } diff --git a/src/vs/workbench/api/electron-browser/mainThreadCommands.ts b/src/vs/workbench/api/electron-browser/mainThreadCommands.ts index ad4bfc1165a..3f6db29fc94 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadCommands.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadCommands.ts @@ -94,11 +94,11 @@ function _generateMarkdown(description: string | ICommandHandlerDescription): st parts.push('\n\n'); if (description.args) { for (let arg of description.args) { - parts.push(`* _${arg.name}_ ${arg.description || ''}\n`); + parts.push(`* _${arg.name}_ - ${arg.description || ''}\n`); } } if (description.returns) { - parts.push(`* _(returns)_ ${description.returns}`); + parts.push(`* _(returns)_ - ${description.returns}`); } parts.push('\n\n'); return parts.join(''); 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/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index 29bf1a401c4..7e32ff27498 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -5,7 +5,7 @@ 'use strict'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions } from 'vs/platform/files/common/files'; @@ -71,11 +71,9 @@ class RemoteFileSystemProvider implements IFileSystemProvider { watch(resource: URI, opts: IWatchOptions) { const session = Math.random(); this._proxy.$watch(this._handle, session, resource, opts); - return { - dispose: () => { - this._proxy.$unwatch(this._handle, session); - } - }; + return toDisposable(() => { + this._proxy.$unwatch(this._handle, session); + }); } $onFileSystemChange(changes: IFileChangeDto[]): void { diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 7ecf37c7311..baa55c39fa6 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -162,7 +162,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerImplementationSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.ImplementationProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideImplementation: (model, position, token): Thenable => { - return wireCancellationToken(token, this._proxy.$provideImplementation(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveLocationDto); + return wireCancellationToken(token, this._proxy.$provideImplementation(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveDefinitionLinkDto); } }); } @@ -170,7 +170,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerTypeDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.TypeDefinitionProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideTypeDefinition: (model, position, token): Thenable => { - return wireCancellationToken(token, this._proxy.$provideTypeDefinition(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveLocationDto); + return wireCancellationToken(token, this._proxy.$provideTypeDefinition(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveDefinitionLinkDto); } }); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index 9091dc3412a..59f93eb7f2f 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -215,16 +215,20 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() }); return new Promise((resolve, reject) => { - setTimeout(() => reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)), timeout); - getDocumentFormattingEdits(model, { tabSize, insertSpaces }) - .then(edits => this._editorWorkerService.computeMoreMinimalEdits(model.uri, edits)) - .then(resolve, err => { - if (!(err instanceof Error) || err.name !== NoProviderError.Name) { - reject(err); - } else { - resolve(); - } - }); + let request = getDocumentFormattingEdits(model, { tabSize, insertSpaces }); + + setTimeout(() => { + reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); + request.cancel(); + }, timeout); + + request.then(edits => this._editorWorkerService.computeMoreMinimalEdits(model.uri, edits)).then(resolve, err => { + if (!(err instanceof Error) || err.name !== NoProviderError.Name) { + reject(err); + } else { + resolve(); + } + }); }).then(edits => { if (!isFalsyOrEmpty(edits) && versionNow === model.getVersionId()) { diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 45c911be480..cafb5b305b2 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,10 +329,10 @@ 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), + _id: id, // uuidMap.getUUID(identifier) _source: source, _label: label, type: definition.type, @@ -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/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 242091da9b1..ccf129ce117 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -178,7 +178,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape }; this._proxy.$createProcess(request.proxy.terminalId, shellLaunchConfigDto, request.cols, request.rows); request.proxy.onInput(data => this._proxy.$acceptProcessInput(request.proxy.terminalId, data)); - request.proxy.onResize((cols, rows) => this._proxy.$acceptProcessResize(request.proxy.terminalId, cols, rows)); + request.proxy.onResize(dimensions => this._proxy.$acceptProcessResize(request.proxy.terminalId, dimensions.cols, dimensions.rows)); request.proxy.onShutdown(() => this._proxy.$acceptProcessShutdown(request.proxy.terminalId)); } 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/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 371e4abab23..8db9fa2331f 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -10,8 +10,8 @@ import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFileMatch, IFolderQuery, IPatternInfo, IQueryOptions, ISearchConfiguration, ISearchQuery, ISearchService, QueryType } from 'vs/platform/search/common/search'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IFileMatch, IFolderQuery, IPatternInfo, IQueryOptions, ISearchConfiguration, ISearchQuery, ISearchService, QueryType, ISearchProgressItem } from 'vs/platform/search/common/search'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @@ -19,6 +19,10 @@ import { QueryBuilder } from 'vs/workbench/parts/search/common/queryBuilder'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape } from '../node/extHost.protocol'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IWindowService } from 'vs/platform/windows/common/windows'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -169,7 +173,13 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { const query = queryBuilder.text(pattern, folders, options); return new TPromise((resolve, reject) => { - const search = this._searchService.search(query).then( + const onProgress = (p: ISearchProgressItem) => { + if (p.lineMatches) { + this._proxy.$handleTextSearchResult(p, requestId); + } + }; + + const search = this._searchService.search(query, onProgress).then( () => { delete this._activeSearches[requestId]; resolve(null); @@ -181,11 +191,6 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } return undefined; - }, - p => { - if (p.lineMatches) { - this._proxy.$handleTextSearchResult(p, requestId); - } }); this._activeSearches[requestId] = search; @@ -210,3 +215,19 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { }); } } + +CommandsRegistry.registerCommand('_workbench.enterWorkspace', async function (accessor: ServicesAccessor, workspace: URI, disableExtensions: string[]) { + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + const extensionService = accessor.get(IExtensionService); + const windowService = accessor.get(IWindowService); + + if (disableExtensions && disableExtensions.length) { + const runningExtensions = await extensionService.getExtensions(); + // If requested extension to disable is running, then reload window with given workspace + if (disableExtensions && runningExtensions.some(runningExtension => disableExtensions.some(id => areSameExtensions({ id }, { id: runningExtension.id })))) { + return windowService.openWindow([URI.file(workspace.fsPath)], { args: { _: [], 'disable-extension': disableExtensions } }); + } + } + + return workspaceEditingService.enterWorkspace(workspace.fsPath); +}); \ No newline at end of file diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/node/apiCommands.ts index 049f955bb8b..98eaf7888ca 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/node/apiCommands.ts @@ -49,7 +49,7 @@ export class OpenFolderAPICommand { return executor.executeCommand('_files.pickFolderAndOpen', forceNewWindow); } - return executor.executeCommand('_files.windowOpen', [uri.fsPath], forceNewWindow); + return executor.executeCommand('_files.windowOpen', [uri], forceNewWindow); } } CommandsRegistry.registerCommand(OpenFolderAPICommand.ID, adjustHandler(OpenFolderAPICommand.execute)); diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 9911e27bbe8..ed91294b6c5 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -452,20 +452,20 @@ 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); - }), - get quickInputBackButton() { - return proposedApiFunction(extension, (): vscode.QuickInputButton => { - return extHostQuickOpen.backButton; - })(); + registerUriHandler(handler: vscode.UriHandler) { + return extHostUrls.registerUriHandler(extension.id, handler); }, - createQuickPick: proposedApiFunction(extension, (): vscode.QuickPick => { + createQuickPick(): vscode.QuickPick { return extHostQuickOpen.createQuickPick(extension.id); - }), - createInputBox: proposedApiFunction(extension, (): vscode.InputBox => { + }, + createInputBox(): vscode.InputBox { return extHostQuickOpen.createInputBox(extension.id); - }), + }, + }; + + // namespace: QuickInputButtons + const QuickInputButtons: typeof vscode.QuickInputButtons = { + Back: extHostQuickOpen.backButton, }; // namespace: workspace @@ -500,7 +500,19 @@ export function createApiFactory( findFiles: (include, exclude, maxResults?, token?) => { return extHostWorkspace.findFiles(typeConverters.GlobPattern.from(include), typeConverters.GlobPattern.from(exclude), maxResults, extension.id, token); }, - findTextInFiles: (query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, token?: vscode.CancellationToken) => { + findTextInFiles: (query: vscode.TextSearchQuery, optionsOrCallback, callbackOrToken?, token?: vscode.CancellationToken) => { + let options: vscode.FindTextInFilesOptions; + let callback: (result: vscode.TextSearchResult) => void; + + if (typeof optionsOrCallback === 'object') { + options = optionsOrCallback; + callback = callbackOrToken; + } else { + options = {}; + callback = optionsOrCallback; + token = callbackOrToken; + } + return extHostWorkspace.findTextInFiles(query, options || {}, callback, extension.id, token); }, saveAll: (includeUntitled?) => { @@ -710,6 +722,7 @@ export function createApiFactory( OverviewRulerLane: OverviewRulerLane, ParameterInformation: extHostTypes.ParameterInformation, Position: extHostTypes.Position, + QuickInputButtons, Range: extHostTypes.Range, Selection: extHostTypes.Selection, SignatureHelp: extHostTypes.SignatureHelp, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index c124332150f..44947586fd5 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -46,7 +46,6 @@ export interface IEnvironment { isExtensionDevelopmentDebug: boolean; appRoot: string; appSettingsHome: string; - disableExtensions: boolean; extensionDevelopmentPath: string; extensionTestsPath: string; } @@ -453,8 +452,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 { @@ -812,8 +811,8 @@ export interface ExtHostLanguageFeaturesShape { $provideCodeLenses(handle: number, resource: UriComponents): TPromise; $resolveCodeLens(handle: number, resource: UriComponents, symbol: modes.ICodeLensSymbol): TPromise; $provideDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideImplementation(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise; + $provideImplementation(handle: number, resource: UriComponents, position: IPosition): TPromise; + $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideHover(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext): TPromise; @@ -878,7 +877,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/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 37cd6a1908d..46087963dda 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -253,7 +253,7 @@ export class ExtHostApiCommands { }); this._register(SetEditorLayoutAPICommand.ID, adjustHandler(SetEditorLayoutAPICommand.execute), { - description: 'Sets the editor layout. The layout is described as object with an initial (optional) orientation (0 = horizontal, 1 = vertical) and an array of editor groups within. Each editor group can have a size and another array of editor groups that will be layed out orthogonal to the orientation. If editor group sizes are provided, their sum must be 1 to be applied per row or column. Example for a 2x2 grid: `{ orientation: 0, groups: [{ groups: [{}, {}], size: 0.5 }, { groups: [{}, {}], size: 0.5 }] }`', + description: 'Sets the editor layout. The layout is described as object with an initial (optional) orientation (0 = horizontal, 1 = vertical) and an array of editor groups within. Each editor group can have a size and another array of editor groups that will be laid out orthogonal to the orientation. If editor group sizes are provided, their sum must be 1 to be applied per row or column. Example for a 2x2 grid: `{ orientation: 0, groups: [{ groups: [{}, {}], size: 0.5 }, { groups: [{}, {}], size: 0.5 }] }`', args: [ { name: 'layout', description: 'The editor layout to set.', constraint: (value: EditorGroupLayout) => typeof value === 'object' && Array.isArray(value.groups) } ] diff --git a/src/vs/workbench/api/node/extHostDiagnostics.ts b/src/vs/workbench/api/node/extHostDiagnostics.ts index 9056054629e..75b3d976752 100644 --- a/src/vs/workbench/api/node/extHostDiagnostics.ts +++ b/src/vs/workbench/api/node/extHostDiagnostics.ts @@ -18,15 +18,17 @@ import { keys } from 'vs/base/common/map'; export class DiagnosticCollection implements vscode.DiagnosticCollection { private readonly _name: string; + private readonly _owner: string; private readonly _maxDiagnosticsPerFile: number; private readonly _onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>; + private readonly _proxy: MainThreadDiagnosticsShape; - private _proxy: MainThreadDiagnosticsShape; private _isDisposed = false; private _data = new Map(); - constructor(name: string, maxDiagnosticsPerFile: number, proxy: MainThreadDiagnosticsShape, onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>) { + constructor(name: string, owner: string, maxDiagnosticsPerFile: number, proxy: MainThreadDiagnosticsShape, onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>) { this._name = name; + this._owner = owner; this._maxDiagnosticsPerFile = maxDiagnosticsPerFile; this._proxy = proxy; this._onDidChangeDiagnostics = onDidChangeDiagnostics; @@ -35,8 +37,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { dispose(): void { if (!this._isDisposed) { this._onDidChangeDiagnostics.fire(keys(this._data)); - this._proxy.$clear(this.name); - this._proxy = undefined; + this._proxy.$clear(this._owner); this._data = undefined; this._isDisposed = true; } @@ -142,21 +143,21 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { } this._onDidChangeDiagnostics.fire(toSync); - this._proxy.$changeMany(this.name, entries); + this._proxy.$changeMany(this._owner, entries); } delete(uri: vscode.Uri): void { this._checkDisposed(); this._onDidChangeDiagnostics.fire([uri]); this._data.delete(uri.toString()); - this._proxy.$changeMany(this.name, [[uri, undefined]]); + this._proxy.$changeMany(this._owner, [[uri, undefined]]); } clear(): void { this._checkDisposed(); this._onDidChangeDiagnostics.fire(keys(this._data)); this._data.clear(); - this._proxy.$clear(this.name); + this._proxy.$clear(this._owner); } forEach(callback: (uri: URI, diagnostics: vscode.Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void { @@ -204,7 +205,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private static readonly _maxDiagnosticsPerFile: number = 1000; private readonly _proxy: MainThreadDiagnosticsShape; - private readonly _collections: DiagnosticCollection[] = []; + private readonly _collections = new Map(); private readonly _onDidChangeDiagnostics = new Emitter<(vscode.Uri | string)[]>(); static _debouncer(last: (vscode.Uri | string)[], current: (vscode.Uri | string)[]): (vscode.Uri | string)[] { @@ -242,22 +243,28 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } createDiagnosticCollection(name: string): vscode.DiagnosticCollection { + let { _collections, _proxy, _onDidChangeDiagnostics } = this; + let owner: string; if (!name) { name = '_generated_diagnostic_collection_name_#' + ExtHostDiagnostics._idPool++; + owner = name; + } else if (!_collections.has(name)) { + owner = name; + } else { + console.warn(`DiagnosticCollection with name '${name}' does already exist.`); + do { + owner = name + ExtHostDiagnostics._idPool++; + } while (_collections.has(owner)); } - const { _collections, _proxy, _onDidChangeDiagnostics } = this; const result = new class extends DiagnosticCollection { constructor() { - super(name, ExtHostDiagnostics._maxDiagnosticsPerFile, _proxy, _onDidChangeDiagnostics); - _collections.push(this); + super(name, owner, ExtHostDiagnostics._maxDiagnosticsPerFile, _proxy, _onDidChangeDiagnostics); + _collections.set(owner, this); } dispose() { super.dispose(); - let idx = _collections.indexOf(this); - if (idx !== -1) { - _collections.splice(idx, 1); - } + _collections.delete(owner); } }; @@ -272,7 +279,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } else { let index = new Map(); let res: [vscode.Uri, vscode.Diagnostic[]][] = []; - for (const collection of this._collections) { + this._collections.forEach(collection => { collection.forEach((uri, diagnostics) => { let idx = index.get(uri.toString()); if (typeof idx === 'undefined') { @@ -282,18 +289,18 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } res[idx][1] = res[idx][1].concat(...diagnostics); }); - } + }); return res; } } private _getDiagnostics(resource: vscode.Uri): vscode.Diagnostic[] { let res: vscode.Diagnostic[] = []; - for (const collection of this._collections) { + this._collections.forEach(collection => { if (collection.has(resource)) { res = res.concat(collection.get(resource)); } - } + }); return res; } } diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts index 1b1d65efcea..d40aad904b9 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from './extHost.protocol'; import * as vscode from 'vscode'; import * as files from 'vs/platform/files/common/files'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { asWinJsPromise } from 'vs/base/common/async'; import { values } from 'vs/base/common/map'; import { Range, FileChangeType } from 'vs/workbench/api/node/extHostTypes'; @@ -132,15 +132,13 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._proxy.$onFileSystemChange(handle, mapped); }); - return { - dispose: () => { - subscription.dispose(); - this._linkProvider.delete(scheme); - this._usedSchemes.delete(scheme); - this._fsProvider.delete(handle); - this._proxy.$unregisterProvider(handle); - } - }; + return toDisposable(() => { + subscription.dispose(); + this._linkProvider.delete(scheme); + this._usedSchemes.delete(scheme); + this._fsProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); } private static _asIStat(stat: vscode.FileStat): files.IStat { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index f562cb6d6fe..8dc9a268ce5 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -143,6 +143,15 @@ class CodeLensAdapter { } } +function convertToDefinitionLinks(value: vscode.Definition): modes.DefinitionLink[] { + if (Array.isArray(value)) { + return (value as (vscode.DefinitionLink | vscode.Location)[]).map(typeConvert.DefinitionLink.from); + } else if (value) { + return [typeConvert.DefinitionLink.from(value)]; + } + return undefined; +} + class DefinitionAdapter { constructor( @@ -153,29 +162,7 @@ class DefinitionAdapter { provideDefinition(resource: URI, position: IPosition): TPromise { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - - return asWinJsPromise(token => this._provider.provideDefinition2 ? this._provider.provideDefinition2(doc, pos, token) : this._provider.provideDefinition(doc, pos, token)).then((value): modes.DefinitionLink[] => { - if (Array.isArray(value)) { - return (value as (vscode.DefinitionLink | vscode.Location)[]).map(x => DefinitionAdapter.convertDefinitionLink(x)); - } else if (value) { - return [DefinitionAdapter.convertDefinitionLink(value)]; - } - return undefined; - }); - } - - private static convertDefinitionLink(value: vscode.Location | vscode.DefinitionLink): modes.DefinitionLink { - const definitionLink = value; - return { - origin: definitionLink.origin - ? typeConvert.Range.from(definitionLink.origin) - : undefined, - uri: value.uri, - range: typeConvert.Range.from(value.range), - selectionRange: definitionLink.selectionRange - ? typeConvert.Range.from(definitionLink.selectionRange) - : undefined, - }; + return asWinJsPromise(token => this._provider.provideDefinition(doc, pos, token)).then(convertToDefinitionLinks); } } @@ -186,17 +173,10 @@ class ImplementationAdapter { private readonly _provider: vscode.ImplementationProvider ) { } - provideImplementation(resource: URI, position: IPosition): TPromise { + provideImplementation(resource: URI, position: IPosition): TPromise { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideImplementation(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return value.map(typeConvert.location.from); - } else if (value) { - return typeConvert.location.from(value); - } - return undefined; - }); + return asWinJsPromise(token => this._provider.provideImplementation(doc, pos, token)).then(convertToDefinitionLinks); } } @@ -207,17 +187,10 @@ class TypeDefinitionAdapter { private readonly _provider: vscode.TypeDefinitionProvider ) { } - provideTypeDefinition(resource: URI, position: IPosition): TPromise { + provideTypeDefinition(resource: URI, position: IPosition): TPromise { const doc = this._documents.getDocumentData(resource).document; const pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideTypeDefinition(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return value.map(typeConvert.location.from); - } else if (value) { - return typeConvert.location.from(value); - } - return undefined; - }); + return asWinJsPromise(token => this._provider.provideTypeDefinition(doc, pos, token)).then(convertToDefinitionLinks); } } @@ -999,7 +972,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideImplementation(handle: number, resource: UriComponents, position: IPosition): TPromise { + $provideImplementation(handle: number, resource: UriComponents, position: IPosition): TPromise { return this._withAdapter(handle, ImplementationAdapter, adapter => adapter.provideImplementation(URI.revive(resource), position)); } @@ -1009,7 +982,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise { + $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise { return this._withAdapter(handle, TypeDefinitionAdapter, adapter => adapter.provideTypeDefinition(URI.revive(resource), position)); } diff --git a/src/vs/workbench/api/node/extHostProgress.ts b/src/vs/workbench/api/node/extHostProgress.ts index fc4be5b66e6..0700313c48c 100644 --- a/src/vs/workbench/api/node/extHostProgress.ts +++ b/src/vs/workbench/api/node/extHostProgress.ts @@ -71,11 +71,14 @@ export class ExtHostProgress implements ExtHostProgressShape { function mergeProgress(result: IProgressStep, currentValue: IProgressStep): IProgressStep { result.message = currentValue.message; - if (typeof currentValue.increment === 'number' && typeof result.message === 'number') { - result.increment += currentValue.increment; - } else if (typeof currentValue.increment === 'number') { - result.increment = currentValue.increment; + if (typeof currentValue.increment === 'number') { + if (typeof result.increment === 'number') { + result.increment += currentValue.increment; + } else { + result.increment = currentValue.increment; + } } + return result; } diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index d35c2401007..35f121e588c 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -9,14 +9,13 @@ 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 * 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 { toDisposable } from 'vs/base/common/lifecycle'; export interface ISchemeTransformer { transformOutgoing(scheme: string): string; @@ -30,9 +29,9 @@ export class ExtHostSearch implements ExtHostSearchShape { private _fileSearchManager: FileSearchManager; - constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs, private _pfs = pfs) { + constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs) { this._proxy = mainContext.getProxy(MainContext.MainThreadSearch); - this._fileSearchManager = new FileSearchManager(this._pfs); + this._fileSearchManager = new FileSearchManager(); } private _transformScheme(scheme: string): string { @@ -46,12 +45,10 @@ export class ExtHostSearch implements ExtHostSearchShape { const handle = this._handlePool++; this._searchProvider.set(handle, provider); this._proxy.$registerSearchProvider(handle, this._transformScheme(scheme)); - return { - dispose: () => { - this._searchProvider.delete(handle); - this._proxy.$unregisterProvider(handle); - } - }; + return toDisposable(() => { + this._searchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); } $provideFileSearchResults(handle: number, session: number, rawQuery: IRawSearchQuery): TPromise { @@ -317,12 +314,12 @@ class QueryGlobTester { /** * Guaranteed sync - siblingsFn should not return a promise. */ - public includedInQuerySync(testPath: string, basename?: string, siblingsFn?: () => string[]): boolean { - if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, siblingsFn)) { + public includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean { + if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, hasSibling)) { return false; } - if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, siblingsFn)) { + if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, hasSibling)) { return false; } @@ -332,9 +329,9 @@ class QueryGlobTester { /** * Guaranteed async. */ - public includedInQuery(testPath: string, basename?: string, siblingsFn?: () => string[] | TPromise): TPromise { + public includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): TPromise { const excludeP = this._parsedExcludeExpression ? - TPromise.as(this._parsedExcludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : + TPromise.as(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result) : TPromise.wrap(false); return excludeP.then(excluded => { @@ -343,7 +340,7 @@ class QueryGlobTester { } return this._parsedIncludeExpression ? - TPromise.as(this._parsedIncludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : + TPromise.as(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) : TPromise.wrap(true); }).then(included => { return included; @@ -429,13 +426,13 @@ class TextSearchEngine { const testingPs = []; const progress = { report: (result: vscode.TextSearchResult) => { - const siblingFn = folderQuery.folder.scheme === 'file' && (() => { + const hasSibling = folderQuery.folder.scheme === 'file' && glob.hasSiblingPromiseFn(() => { return this.readdir(path.dirname(result.uri.fsPath)); }); const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath); testingPs.push( - queryTester.includedInQuery(relativePath, path.basename(relativePath), siblingFn) + queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling) .then(included => { if (included) { onResult(result); @@ -506,7 +503,6 @@ function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery { class FileSearchEngine { private filePattern: string; - private normalizedFilePatternLowercase: string; private includePattern: glob.ParsedExpression; private maxResults: number; private exists: boolean; @@ -518,7 +514,7 @@ class FileSearchEngine { private globalExcludePattern: glob.ParsedExpression; - constructor(private config: ISearchQuery, private provider: vscode.SearchProvider, private _pfs: typeof pfs) { + constructor(private config: ISearchQuery, private provider: vscode.SearchProvider) { this.filePattern = config.filePattern; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || null; @@ -527,10 +523,6 @@ class FileSearchEngine { this.isLimitHit = false; this.activeCancellationTokens = new Set(); - if (this.filePattern) { - this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase(); - } - this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern); } @@ -633,19 +625,6 @@ class FileSearchEngine { return null; } - if (noSiblingsClauses && this.isLimitHit) { - // 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); return null; }).then( @@ -712,6 +691,7 @@ class FileSearchEngine { const self = this; const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -720,7 +700,7 @@ class FileSearchEngine { // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - if (!queryTester.includedInQuerySync(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) { + if (!queryTester.includedInQuerySync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -743,26 +723,8 @@ class FileSearchEngine { matchDirectory(rootEntries); } - private checkFilePatternRelativeMatch(base: URI): TPromise<{ exists: boolean, size?: number }> { - if (!this.filePattern || path.isAbsolute(this.filePattern) || base.scheme !== 'file') { - return TPromise.wrap({ exists: false }); - } - - const absolutePath = path.join(base.fsPath, this.filePattern); - return this._pfs.stat(absolutePath).then(stat => { - return { - exists: !stat.isDirectory(), - size: stat.size - }; - }, err => { - return { - exists: false - }; - }); - } - private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void { - if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) { + if (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename)) { if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) { this.isLimitHit = true; this.cancel(); @@ -773,20 +735,6 @@ class FileSearchEngine { } } } - - private isFilePatternMatch(path: string): boolean { - // Check for search pattern - if (this.filePattern) { - if (this.filePattern === '*') { - return true; // support the all-matching wildcard - } - - return strings.fuzzyContains(path, this.normalizedFilePatternLowercase); - } - - // No patterns means we match all - return true; - } } interface IInternalSearchComplete { @@ -800,12 +748,10 @@ class FileSearchManager { private readonly expandedCacheKeys = new Map(); - constructor(private _pfs: typeof pfs) { } - fileSearch(config: ISearchQuery, provider: vscode.SearchProvider): PPromise { let searchP: PPromise; return new PPromise((c, e, p) => { - const engine = new FileSearchEngine(config, provider, this._pfs); + const engine = new FileSearchEngine(config, provider); searchP = this.doSearch(engine, FileSearchManager.BATCH_SIZE).then( result => { diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 386a0ed6580..ee825674099 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -860,15 +860,24 @@ 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 { + sanitized.push(task); + console.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`); + } + } 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/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 956f724057e..55ecfbfe88d 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -5,17 +5,15 @@ 'use strict'; import * as vscode from 'vscode'; -import * as cp from 'child_process'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; -import Uri from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/node/extHost.protocol'; -import { IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ILogService } from 'vs/platform/log/common/log'; import { EXT_HOST_CREATION_DELAY } from 'vs/workbench/parts/terminal/common/terminal'; +import { TerminalProcess } from 'vs/workbench/parts/terminal/node/terminalProcess'; const RENDERER_NO_PROCESS_ID = -1; @@ -226,7 +224,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _proxy: MainThreadTerminalServiceShape; private _activeTerminal: ExtHostTerminal; private _terminals: ExtHostTerminal[] = []; - private _terminalProcesses: { [id: number]: cp.ChildProcess } = {}; + private _terminalProcesses: { [id: number]: TerminalProcess } = {}; private _terminalRenderers: ExtHostTerminalRenderer[] = []; public get activeTerminal(): ExtHostTerminal { return this._activeTerminal; } @@ -359,7 +357,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { const terminalConfig = this._extHostConfiguration.getConfiguration('terminal.integrated'); - const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; if (!shellLaunchConfig.executable) { // TODO: This duplicates some of TerminalConfigHelper.mergeDefaultShellPathAndArgs and should be merged // this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); @@ -383,61 +380,48 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); // const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); - // shellLaunchConfig.env = envFromShell; // Merge process env with the env from config - const parentEnv = { ...process.env }; - // terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + const env = { ...process.env }; + // terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); // Continue env initialization, merging in the env from the launch // config and adding keys that are needed to create the process - const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, initialCwd, locale, cols, rows); - const cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; - const options = { env, cwd, execArgv: [] }; + const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); // Fork the process and listen for messages - this._logService.debug(`Terminal process launching on ext host`, options); - this._terminalProcesses[id] = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); - this._terminalProcesses[id].on('message', (message: IMessageFromTerminalProcess) => { - switch (message.type) { - case 'pid': this._proxy.$sendProcessPid(id, message.content); break; - case 'title': this._proxy.$sendProcessTitle(id, message.content); break; - case 'data': this._proxy.$sendProcessData(id, message.content); break; - } - }); - this._terminalProcesses[id].on('exit', (exitCode) => this._onProcessExit(id, exitCode)); + this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env); + this._terminalProcesses[id] = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env); + this._terminalProcesses[id].onProcessIdReady(pid => this._proxy.$sendProcessPid(id, pid)); + this._terminalProcesses[id].onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); + this._terminalProcesses[id].onProcessData(data => this._proxy.$sendProcessData(id, data)); + this._terminalProcesses[id].onProcessExit((exitCode) => this._onProcessExit(id, exitCode)); } public $acceptProcessInput(id: number, data: string): void { - if (this._terminalProcesses[id].connected) { - this._terminalProcesses[id].send({ event: 'input', data }); - } + this._terminalProcesses[id].input(data); } public $acceptProcessResize(id: number, cols: number, rows: number): void { - if (this._terminalProcesses[id].connected) { - try { - this._terminalProcesses[id].send({ event: 'resize', cols, rows }); - } catch (error) { - // We tried to write to a closed pipe / channel. - if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { - throw (error); - } + try { + this._terminalProcesses[id].resize(cols, rows); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); } } } public $acceptProcessShutdown(id: number): void { - if (this._terminalProcesses[id].connected) { - this._terminalProcesses[id].send({ event: 'shutdown' }); - } + this._terminalProcesses[id].shutdown(); } private _onProcessExit(id: number, exitCode: number): void { // Remove listeners - const process = this._terminalProcesses[id]; - process.removeAllListeners('message'); - process.removeAllListeners('exit'); + this._terminalProcesses[id].dispose(); // Remove process reference delete this._terminalProcesses[id]; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 96b275af75f..ae449946df0 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -413,6 +413,23 @@ export const location = { } }; +export namespace DefinitionLink { + export function from(value: vscode.Location | vscode.DefinitionLink): modes.DefinitionLink { + const definitionLink = value; + const location = value; + return { + origin: definitionLink.originSelectionRange + ? Range.from(definitionLink.originSelectionRange) + : undefined, + uri: definitionLink.targetUri ? definitionLink.targetUri : location.uri, + range: Range.from(definitionLink.targetRange ? definitionLink.targetRange : location.range), + selectionRange: definitionLink.targetSelectionRange + ? Range.from(definitionLink.targetSelectionRange) + : undefined, + }; + } +} + export namespace Hover { export function from(hover: vscode.Hover): modes.Hover { return { @@ -512,6 +529,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/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 81e6a53db9a..4bf2cd070ba 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -498,6 +498,7 @@ export class TextEdit { export interface IFileOperationOptions { overwrite?: boolean; ignoreIfExists?: boolean; + ignoreIfNotExists?: boolean; recursive?: boolean; } @@ -518,7 +519,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { private _edits = new Array(); - renameFile(from: vscode.Uri, to: vscode.Uri, options?: { overwrite?: boolean }): void { + renameFile(from: vscode.Uri, to: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }): void { this._edits.push({ _type: 1, from, to, options }); } @@ -526,7 +527,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this._edits.push({ _type: 1, from: undefined, to: uri, options }); } - deleteFile(uri: vscode.Uri, options?: { recursive?: boolean }): void { + deleteFile(uri: vscode.Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean }): void { this._edits.push({ _type: 1, from: uri, to: undefined, options }); } 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 2afd565ed8a..ae1f6739927 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -268,6 +268,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { if (folders.length === 0) { return undefined; } + // #54483 @Joh Why are we still using fsPath? return folders[0].uri.fsPath; } diff --git a/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts b/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts index bb784e89472..9f504717d73 100644 --- a/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; @@ -40,4 +40,13 @@ export class ToggleActivityBarVisibilityAction extends Action { } const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, ToggleActivityBarVisibilityAction.LABEL), 'View: Toggle Activity Bar Visibility', nls.localize('view', "View")); \ No newline at end of file +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, ToggleActivityBarVisibilityAction.LABEL), 'View: Toggle Activity Bar Visibility', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleActivityBarVisibilityAction.ID, + title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") + }, + order: 4 +}); diff --git a/src/vs/workbench/browser/actions/toggleCenteredLayout.ts b/src/vs/workbench/browser/actions/toggleCenteredLayout.ts index 2ab19d4ca4d..4be449f24ac 100644 --- a/src/vs/workbench/browser/actions/toggleCenteredLayout.ts +++ b/src/vs/workbench/browser/actions/toggleCenteredLayout.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -34,3 +34,21 @@ class ToggleCenteredLayout extends Action { const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleCenteredLayout, ToggleCenteredLayout.ID, ToggleCenteredLayout.LABEL), 'View: Toggle Centered Layout', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleCenteredLayout.ID, + title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutCentered', + title: nls.localize({ key: 'miCenteredEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered") + }, + order: 2 +}); diff --git a/src/vs/workbench/browser/actions/toggleEditorLayout.ts b/src/vs/workbench/browser/actions/toggleEditorLayout.ts index 8f553a8fa22..32b9cc1c921 100644 --- a/src/vs/workbench/browser/actions/toggleEditorLayout.ts +++ b/src/vs/workbench/browser/actions/toggleEditorLayout.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; @@ -73,4 +73,13 @@ CommandsRegistry.registerCommand('_workbench.editor.setGroupOrientation', functi const registry = Registry.as(Extensions.WorkbenchActions); const group = nls.localize('view', "View"); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Flip Editor Group Layout', group); \ No newline at end of file +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Flip Editor Group Layout', group); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: 'z_flip', + command: { + id: ToggleEditorLayoutAction.ID, + title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") + }, + order: 1 +}); diff --git a/src/vs/workbench/browser/actions/toggleSidebarPosition.ts b/src/vs/workbench/browser/actions/toggleSidebarPosition.ts index 3de25a53e7e..3c64a5a1c6d 100644 --- a/src/vs/workbench/browser/actions/toggleSidebarPosition.ts +++ b/src/vs/workbench/browser/actions/toggleSidebarPosition.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IPartService, Position } from 'vs/workbench/services/part/common/partService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -41,3 +41,12 @@ export class ToggleSidebarPositionAction extends Action { const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL), 'View: Toggle Side Bar Position', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") + }, + order: 2 +}); diff --git a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts b/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts index 95f4d8a21e8..e69151cac2b 100644 --- a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -35,4 +35,13 @@ export class ToggleSidebarVisibilityAction extends Action { } const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarVisibilityAction, ToggleSidebarVisibilityAction.ID, ToggleSidebarVisibilityAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', nls.localize('view', "View")); \ No newline at end of file +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarVisibilityAction, ToggleSidebarVisibilityAction.ID, ToggleSidebarVisibilityAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleSidebarVisibilityAction.ID, + title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") + }, + order: 1 +}); diff --git a/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts b/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts index 2fe83369bb8..2ec27e5f765 100644 --- a/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; @@ -40,4 +40,13 @@ export class ToggleStatusbarVisibilityAction extends Action { } const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, ToggleStatusbarVisibilityAction.LABEL), 'View: Toggle Status Bar Visibility', nls.localize('view', "View")); \ No newline at end of file +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, ToggleStatusbarVisibilityAction.LABEL), 'View: Toggle Status Bar Visibility', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleStatusbarVisibilityAction.ID, + title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") + }, + order: 3 +}); diff --git a/src/vs/workbench/browser/actions/toggleZenMode.ts b/src/vs/workbench/browser/actions/toggleZenMode.ts index 955a69799ce..a13035026da 100644 --- a/src/vs/workbench/browser/actions/toggleZenMode.ts +++ b/src/vs/workbench/browser/actions/toggleZenMode.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -33,4 +33,13 @@ class ToggleZenMode extends Action { } const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleZenMode, ToggleZenMode.ID, ToggleZenMode.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) }), 'View: Toggle Zen Mode', nls.localize('view', "View")); \ No newline at end of file +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleZenMode, ToggleZenMode.ID, ToggleZenMode.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) }), 'View: Toggle Zen Mode', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleZenMode.ID, + title: nls.localize('miToggleZenMode', "Toggle Zen Mode") + }, + order: 2 +}); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index bf7b0adc209..985be405ccf 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -19,6 +19,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ICommandService } from 'vs/platform/commands/common/commands'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID, defaultWorkspacePath, defaultFilePath, defaultFolderPath } from 'vs/workbench/browser/actions/workspaceCommands'; +import URI from 'vs/base/common/uri'; export class OpenFileAction extends Action { @@ -239,7 +240,7 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { return this.workspacesService.createWorkspace(folders).then(newWorkspace => { return this.workspaceEditingService.copyWorkspaceSettings(newWorkspace).then(() => { - return this.windowService.openWindow([newWorkspace.configPath], { forceNewWindow: true }); + return this.windowService.openWindow([URI.file(newWorkspace.configPath)], { forceNewWindow: true }); }); }); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 1eff3ded937..8db6cf1d71e 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -290,16 +290,16 @@ export class ResourcesDropHandler { // Pass focus to window this.windowService.focusWindow(); - let workspacesToOpen: TPromise; + let workspacesToOpen: TPromise; // Open in separate windows if we drop workspaces or just one folder if (workspaces.length > 0 || folders.length === 1) { - workspacesToOpen = TPromise.as([...workspaces, ...folders].map(resources => resources.fsPath)); + workspacesToOpen = TPromise.as([...workspaces, ...folders].map(resources => resources)); } // Multiple folders: Create new workspace with folders and open else if (folders.length > 1) { - workspacesToOpen = this.workspacesService.createWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [workspace.configPath]); + workspacesToOpen = this.workspacesService.createWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [URI.file(workspace.configPath)]); } // Open diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 0db9f7aae65..35768b3f5fa 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -199,11 +199,12 @@ export class ResourceLabel extends IconLabel { iconLabelOptions.title = this.computedPathLabel; } - if (!this.computedIconClasses) { - this.computedIconClasses = getIconClasses(this.modelService, this.modeService, resource, this.options && this.options.fileKind); + if (this.options && !this.options.hideIcon) { + if (!this.computedIconClasses) { + this.computedIconClasses = getIconClasses(this.modelService, this.modeService, resource, this.options && this.options.fileKind); + } + iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); } - - iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); if (this.options && this.options.extraClasses) { iconLabelOptions.extraClasses.push(...this.options.extraClasses); } @@ -341,15 +342,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/media/part.css b/src/vs/workbench/browser/media/part.css index 94f68267136..f9f85749d90 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -30,9 +30,12 @@ padding-left: 12px; } -.monaco-workbench > .part > .title > .title-label span { +.monaco-workbench > .part > .title > .title-label h2 { font-size: 11px; cursor: default; + font-weight: normal; + -webkit-margin-before: 0; + -webkit-margin-after: 0; } .monaco-workbench > .part > .title > .title-label a { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 386e2981306..52578559e00 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -434,7 +434,7 @@ export abstract class CompositePart extends Part { $(parent).div({ 'class': 'title-label' }, div => { - titleLabel = div.span(); + titleLabel = div.element('h2'); }); const $this = this; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts new file mode 100644 index 00000000000..ac7b2f27130 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { BreadcrumbsWidget } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { GroupIdentifier } from 'vs/workbench/common/editor'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { localize } from 'vs/nls'; + +export const IBreadcrumbsService = createDecorator('IEditorBreadcrumbsService'); + +export interface IBreadcrumbsService { + + _serviceBrand: any; + + register(group: GroupIdentifier, widget: BreadcrumbsWidget): IDisposable; + + getWidget(group: GroupIdentifier): BreadcrumbsWidget; +} + + +export class BreadcrumbsService implements IBreadcrumbsService { + + _serviceBrand: any; + + private readonly _map = new Map(); + + register(group: number, widget: BreadcrumbsWidget): IDisposable { + if (this._map.has(group)) { + throw new Error(`group (${group}) has already a widget`); + } + this._map.set(group, widget); + return { + dispose: () => this._map.delete(group) + }; + } + + getWidget(group: number): BreadcrumbsWidget { + return this._map.get(group); + } +} + +registerSingleton(IBreadcrumbsService, BreadcrumbsService); + + +//#region config + +export abstract class BreadcrumbsConfig { + + name: string; + value: T; + onDidChange: Event; + abstract dispose(): void; + + private constructor() { + // internal + } + + static IsEnabled = BreadcrumbsConfig._stub('breadcrumbs.enabled'); + static UseQuickPick = BreadcrumbsConfig._stub('breadcrumbs.useQuickPick'); + static FilePath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.filePath'); + static SymbolPath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.symbolPath'); + + private static _stub(name: string): { bindTo(service: IConfigurationService): BreadcrumbsConfig } { + return { + bindTo(service) { + 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; + }, + set value(newValue: T) { + service.updateValue(name, newValue); + value = newValue; + }, + 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 breadcrumbs"), + type: 'boolean', + default: false + }, + 'breadcrumbs.useQuickPick': { + description: localize('useQuickPick', "Use quick pick instead of breadcrumb-pickers."), + type: 'boolean', + default: false + }, + 'breadcrumbs.filePath': { + description: localize('filepath', "Controls if and how file paths are shown in the breadcrumbs view."), + type: 'string', + default: 'on', + enum: ['on', 'off', 'last'], + enumDescriptions: [ + localize('filepath.on', "Show the file path in the breadcrumbs view."), + localize('filepath.off', "Do not show the file path in the breadcrumbs view."), + localize('filepath.last', "Only show the last element of the file path in the breadcrumbs view."), + ] + }, + 'breadcrumbs.symbolPath': { + description: localize('symbolpath', "Controls if and how symbols are shown in the breadcrumbs view."), + type: 'string', + default: 'on', + enum: ['on', 'off', 'last'], + enumDescriptions: [ + localize('symbolpath.on', "Show all symbols the breadcrumbs view."), + localize('symbolpath.off', "Do not show symbols in the breadcrumbs view."), + localize('symbolpath.last', "Only show the current symbol in the breadcrumbs view."), + ] + }, + } +}); + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts new file mode 100644 index 00000000000..c8a2110bfd2 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -0,0 +1,462 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { combinedDisposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import 'vs/css!./media/breadcrumbscontrol'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { symbolKindToCssClass } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind, IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +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 { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { createBreadcrumbsPicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; +import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { WorkbenchListFocusContextKey, IListService } from 'vs/platform/list/browser/listService'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; + +class Item extends BreadcrumbsItem { + + private readonly _disposables: IDisposable[] = []; + + constructor( + readonly element: BreadcrumbElement, + readonly options: IBreadcrumbsControlOptions, + @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, + hideIcon: !this.element.isFile || !this.options.showFileIcons, + fileDecorations: { colors: this.options.showDecorationColors, badges: false } + }); + this._disposables.push(label); + dom.toggleClass(container, 'file', this.element.isFile); + + } else if (this.element instanceof OutlineModel) { + // has outline element but not in one + let label = document.createElement('div'); + label.innerHTML = '…'; + container.appendChild(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 + if (this.options.showSymbolIcons) { + let icon = document.createElement('div'); + icon.className = symbolKindToCssClass(this.element.symbol.kind); + container.appendChild(icon); + dom.addClass(container, 'shows-symbol-icon'); + } + let label = new IconLabel(container); + let title = this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE'); + label.setValue(title, undefined, { title }); + this._disposables.push(label); + } + } +} + +export interface IBreadcrumbsControlOptions { + showFileIcons: boolean; + showSymbolIcons: boolean; + showDecorationColors: boolean; + extraClasses: string[]; +} + +export class BreadcrumbsControl { + + static HEIGHT = 25; + + static readonly Payload_Reveal = {}; + static readonly Payload_Pick = {}; + + static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false); + static CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false); + + private readonly _ckBreadcrumbsVisible: IContextKey; + private readonly _ckBreadcrumbsActive: IContextKey; + + private readonly _cfUseQuickPick: BreadcrumbsConfig; + + readonly domNode: HTMLDivElement; + private readonly _widget: BreadcrumbsWidget; + + private _disposables = new Array(); + private _breadcrumbsDisposables = new Array(); + private _breadcrumbsPickerShowing = false; + + constructor( + container: HTMLElement, + private readonly _options: IBreadcrumbsControlOptions, + private readonly _editorGroup: EditorGroupView, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEditorService private readonly _editorService: IEditorService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IThemeService private readonly _themeService: IThemeService, + @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService, + @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, + ) { + this.domNode = document.createElement('div'); + dom.addClass(this.domNode, 'breadcrumbs-control'); + dom.addClasses(this.domNode, ..._options.extraClasses); + dom.append(container, this.domNode); + + this._widget = new BreadcrumbsWidget(this.domNode); + this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables); + this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables); + this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables); + this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService)); + + this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); + this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); + + this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService); + + this._disposables.push(breadcrumbsService.register(this._editorGroup.id, this._widget)); + } + + dispose(): void { + this._disposables = dispose(this._disposables); + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + this._ckBreadcrumbsVisible.reset(); + this._ckBreadcrumbsActive.reset(); + this._cfUseQuickPick.dispose(); + this._widget.dispose(); + this.domNode.remove(); + } + + layout(dim: dom.Dimension): void { + this._widget.layout(dim); + } + + isHidden(): boolean { + return dom.hasClass(this.domNode, 'hidden'); + } + + hide(): void { + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + this._ckBreadcrumbsVisible.set(false); + dom.toggleClass(this.domNode, 'hidden', true); + } + + update(): boolean { + const input = this._editorGroup.activeEditor; + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + + if (!input || !input.getResource() || (input.getResource().scheme !== Schemas.untitled && !this._fileService.canHandleResource(input.getResource()))) { + // cleanup and return when there is no input or when + // we cannot handle this input + if (!this.isHidden()) { + this.hide(); + return true; + } else { + return false; + } + } + + 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, this._configurationService); + dom.toggleClass(this.domNode, 'relative-path', model.isRelative()); + + let updateBreadcrumbs = () => { + let items = model.getElements().map(element => new Item(element, this._options, this._instantiationService)); + this._widget.setItems(items); + this._widget.reveal(items[items.length - 1]); + }; + let listener = model.onDidUpdate(updateBreadcrumbs); + updateBreadcrumbs(); + this._breadcrumbsDisposables = [model, listener]; + + // close picker on hide/update + this._breadcrumbsDisposables.push({ + dispose: () => { + if (this._breadcrumbsPickerShowing) { + this._contextViewService.hideContextView(); + } + } + }); + + return true; + } + + private _onFocusEvent(event: IBreadcrumbsItemEvent): void { + if (event.item && this._breadcrumbsPickerShowing) { + return this._widget.setSelection(event.item); + } + } + + private _onSelectEvent(event: IBreadcrumbsItemEvent): void { + if (!event.item) { + return; + } + + this._editorGroup.focus(); + const { element } = event.item as Item; + + if (this._shouldRevealItem(event)) { + // reveal the item + this._widget.setFocused(undefined); + this._widget.setSelection(undefined); + this._revealInEditor(event, element); + return; + } + + if (this._cfUseQuickPick.value) { + // using quick pick + this._widget.setFocused(undefined); + this._widget.setSelection(undefined); + this._quickOpenService.show(element instanceof TreeElement ? '@' : ''); + return; + } + + // show picker + let picker: BreadcrumbsPicker; + this._contextViewService.showContextView({ + render: (parent: HTMLElement) => { + picker = createBreadcrumbsPicker(this._instantiationService, parent, element); + let listener = picker.onDidPickElement(data => { + this._contextViewService.hideContextView(); + this._widget.setFocused(undefined); + this._widget.setSelection(undefined); + this._revealInEditor(event, data); + }); + this._breadcrumbsPickerShowing = true; + this._updateCkBreadcrumbsActive(); + + return combinedDisposable([listener, picker]); + }, + getAnchor() { + + let pickerHeight = 330; + let pickerWidth = Math.max(220, dom.getTotalWidth(event.node)); + let pickerArrowSize = 8; + let pickerArrowOffset: number; + + let data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement); + let y = data.top + data.height - pickerArrowSize; + let x = data.left; + if (x + pickerWidth >= window.innerWidth) { + x = window.innerWidth - pickerWidth; + } + if (event.payload instanceof StandardMouseEvent) { + pickerArrowOffset = event.payload.posx - x - pickerArrowSize; + } else { + pickerArrowOffset = (data.left + (data.width * .3)) - x; + } + picker.layout(pickerHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset)); + picker.setInput(element); + return { x, y }; + }, + onHide: () => { + this._breadcrumbsPickerShowing = false; + this._updateCkBreadcrumbsActive(); + } + }); + } + + private _updateCkBreadcrumbsActive(): void { + const value = this._widget.isDOMFocused() || this._breadcrumbsPickerShowing; + this._ckBreadcrumbsActive.set(value); + } + + private _revealInEditor(event: IBreadcrumbsItemEvent, data: any): void { + if (data instanceof FileElement) { + if (data.isFile) { + // open file in editor + this._editorService.openEditor({ resource: data.uri }); + } else { + // show next picker + let items = this._widget.getItems(); + let idx = items.indexOf(event.item); + this._widget.setFocused(items[idx + 1]); + this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); + } + + } else if (data instanceof OutlineElement) { + // open symbol in editor + let model = OutlineModel.get(data); + this._editorService.openEditor({ + resource: model.textModel.uri, + options: { selection: Range.collapseToStart(data.symbol.selectionRange) } + }); + } + } + + private _shouldRevealItem({ payload }: IBreadcrumbsItemEvent): boolean { + return payload === BreadcrumbsControl.Payload_Reveal || (payload instanceof StandardMouseEvent && payload.metaKey); + } +} + +//#region commands + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'breadcrumbs.focus', + title: localize('cmd.focus', "Focus Breadcrumbs") + } +}); +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + order: 99, + command: { + id: 'breadcrumbs.toggle', + title: localize('cmd.toggle', "Toggle Breadcrumbs") + } +}); +CommandsRegistry.registerCommand('breadcrumbs.toggle', accessor => { + let config = accessor.get(IConfigurationService); + let value = BreadcrumbsConfig.IsEnabled.bindTo(config).value; + BreadcrumbsConfig.IsEnabled.bindTo(config).value = !value; +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focus', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + when: BreadcrumbsControl.CK_BreadcrumbsVisible, + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + //todo@joh focus last? + breadcrumbs.getWidget(groups.activeGroup.id).domFocus(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusNext', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.RightArrow, + secondary: [KeyMod.Shift | KeyCode.RightArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).focusNext(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusPrevious', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.LeftArrow, + secondary: [KeyMod.Shift | KeyCode.LeftArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).focusPrev(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectFocused', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Enter, + secondary: [KeyCode.DownArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Pick); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.revealFocused', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyMod.Shift | KeyCode.Enter, + secondary: [KeyCode.Space], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Reveal); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectEditor', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).setFocused(undefined); + breadcrumbs.getWidget(groups.activeGroup.id).setSelection(undefined); + groups.activeGroup.activeControl.focus(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.pickFromTree', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + primary: KeyCode.Tab, + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey), + handler(accessor) { + const list = accessor.get(IListService).lastFocusedList; + if (list instanceof Tree) { + list.setSelection([list.getFocus()]); + } + } +}); +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts new file mode 100644 index 00000000000..f29efc47db1 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * 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, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { Schemas } from 'vs/base/common/network'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; + +export class FileElement { + constructor( + readonly uri: URI, + readonly isFile: boolean + ) { } +} + +export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement; + +type FileInfo = { path: FileElement[], folder: IWorkspaceFolder, showFolder: boolean }; + +export class EditorBreadcrumbsModel { + + private readonly _disposables: IDisposable[] = []; + private readonly _fileInfo: FileInfo; + + private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>; + private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>; + + private _outlineElements: (OutlineModel | 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, + @IConfigurationService configurationService: IConfigurationService, + ) { + + this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService); + this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService); + + this._disposables.push(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); + this._disposables.push(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); + + this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(this._uri, workspaceService); + this._bindToEditor(); + this._onDidUpdate.fire(this); + } + + dispose(): void { + this._cfgFilePath.dispose(); + this._cfgSymbolPath.dispose(); + dispose(this._disposables); + } + + isRelative(): boolean { + return Boolean(this._fileInfo.folder); + } + + getElements(): ReadonlyArray { + let result: BreadcrumbElement[] = []; + + // file path elements + if (this._cfgFilePath.value === 'on') { + result = result.concat(this._fileInfo.path); + } else if (this._cfgFilePath.value === 'last' && this._fileInfo.path.length > 0) { + result = result.concat(this._fileInfo.path.slice(-1)); + } + + // symbol path elements + if (this._cfgSymbolPath.value === 'on') { + result = result.concat(this._outlineElements); + } else if (this._cfgSymbolPath.value === 'last' && this._outlineElements.length > 0) { + result = result.concat(this._outlineElements.slice(-1)); + } + + return result; + } + + private static _initFilePathInfo(uri: URI, workspaceService: IWorkspaceContextService): FileInfo { + + if (uri.scheme === Schemas.untitled) { + return { + showFolder: false, + folder: undefined, + path: [] + }; + } + + let info: FileInfo = { + showFolder: workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE, + folder: workspaceService.getWorkspaceFolder(uri), + path: [] + }; + + while (uri.path !== '/') { + if (info.folder && isEqual(info.folder.uri, uri)) { + break; + } + info.path.unshift(new FileElement(uri, info.path.length === 0)); + uri = uri.with({ path: paths.dirname(uri.path) }); + } + return info; + } + + 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 => { + + // copy the model + model = model.adopt(); + + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => { + timeout.cancelAndSet(() => { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId()) { + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + } + }, 150); + })); + }).catch(err => { + this._updateOutlineElements([]); + onUnexpectedError(err); + }); + } + + private _getOutlineElements(model: OutlineModel, position: IPosition): (OutlineModel | OutlineGroup | OutlineElement)[] { + if (!model) { + return []; + } + let item: OutlineGroup | OutlineElement = model.getItemEnclosingPosition(position); + if (!item) { + return [model]; + } + 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: (OutlineModel | OutlineGroup | OutlineElement)[]): void { + if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { + this._outlineElements = elements; + this._onDidUpdate.fire(this); + } + } + + private static _outlineElementEquals(a: OutlineModel | OutlineGroup | OutlineElement, b: OutlineModel | 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/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts new file mode 100644 index 00000000000..5a49481c2ec --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -0,0 +1,309 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { compareFileNames } from 'vs/base/common/comparers'; +import { Emitter, Event } from 'vs/base/common/event'; +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 } from 'vs/base/parts/tree/browser/tree'; +import 'vs/css!./media/breadcrumbscontrol'; +import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineDataSource, OutlineItemComparator, OutlineRenderer } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { localize } from 'vs/nls'; +import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IInstantiationService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; +import { HighlightingWorkbenchTree, IHighlightingTreeConfiguration, IHighlightingRenderer } from 'vs/platform/list/browser/listService'; +import { IThemeService, DARK } from 'vs/platform/theme/common/themeService'; +import { FileLabel } from 'vs/workbench/browser/labels'; +import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { breadcrumbsPickerBackground } from 'vs/platform/theme/common/colorRegistry'; +import { FuzzyScore, createMatches, fuzzyScore } from 'vs/base/common/filters'; + +export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker { + let ctor: IConstructorSignature1 = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker; + return instantiationService.createInstance(ctor, parent); +} + +export abstract class BreadcrumbsPicker { + + protected readonly _disposables = new Array(); + protected readonly _domNode: HTMLDivElement; + protected readonly _arrow: HTMLDivElement; + protected readonly _treeContainer: HTMLDivElement; + protected readonly _tree: HighlightingWorkbenchTree; + protected readonly _focus: dom.IFocusTracker; + + protected readonly _onDidPickElement = new Emitter(); + + readonly onDidPickElement: Event = this._onDidPickElement.event; + + constructor( + parent: HTMLElement, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IThemeService protected readonly _themeService: IThemeService, + ) { + this._domNode = document.createElement('div'); + this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons'; + parent.appendChild(this._domNode); + + this._focus = dom.trackFocus(this._domNode); + this._focus.onDidBlur(_ => this._onDidPickElement.fire(undefined), undefined, this._disposables); + + const theme = this._themeService.getTheme(); + const color = theme.getColor(breadcrumbsPickerBackground); + + this._arrow = document.createElement('div'); + this._arrow.style.width = '0'; + this._arrow.style.borderStyle = 'solid'; + this._arrow.style.borderWidth = '8px'; + this._arrow.style.borderColor = `transparent transparent ${color.toString()}`; + this._domNode.appendChild(this._arrow); + + this._treeContainer = document.createElement('div'); + this._treeContainer.style.background = color.toString(); + this._treeContainer.style.paddingTop = '2px'; + this._treeContainer.style.boxShadow = `0px 5px 8px ${(theme.type === DARK ? color.darken(.6) : color.darken(.2))}`; + this._domNode.appendChild(this._treeContainer); + + const treeConifg = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined }); + this._tree = this._instantiationService.createInstance( + HighlightingWorkbenchTree, + this._treeContainer, + treeConifg, + { useShadows: false }, + { placeholder: localize('placeholder', "Find") } + ); + this._disposables.push(this._tree.onDidChangeSelection(e => { + if (e.payload !== this._tree) { + setTimeout(_ => this._onDidChangeSelection(e)); // need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click + } + })); + + this._domNode.focus(); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this._focus.dispose(); + } + + setInput(input: any): void { + let actualInput = this._getInput(input); + this._tree.setInput(actualInput).then(() => { + let selection = this._getInitialSelection(this._tree, input); + if (selection) { + this._tree.reveal(selection).then(() => { + this._tree.setSelection([selection], this._tree); + this._tree.setFocus(selection); + this._tree.domFocus(); + }); + } else { + this._tree.focusFirst(); + this._tree.setSelection([this._tree.getFocus()], this._tree); + this._tree.domFocus(); + } + }, onUnexpectedError); + } + + layout(height: number, width: number, arrowSize: number, arrowOffset: number) { + this._domNode.style.height = `${height}px`; + this._domNode.style.width = `${width}px`; + this._arrow.style.borderWidth = `${arrowSize}px`; + this._arrow.style.marginLeft = `${arrowOffset}px`; + + this._treeContainer.style.height = `${height - 2 * arrowSize}px`; + this._treeContainer.style.width = `${width}px`; + this._tree.layout(); + } + + protected abstract _getInput(input: BreadcrumbElement): any; + protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any; + protected abstract _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration; + protected abstract _onDidChangeSelection(e: ISelectionEvent): void; +} + +//#region - Files + +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, IHighlightingRenderer { + + private readonly _scores = new Map(); + + 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, { supportHighlights: true }); + } + + 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 }, + matches: createMatches((this._scores.get(element.resource.toString()) || [, []])[1]) + }); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void { + templateData.dispose(); + } + + updateHighlights(tree: ITree, pattern: string): any { + let nav = tree.getNavigator(undefined, false); + let topScore: FuzzyScore; + let topElement: any; + while (nav.next()) { + let element = nav.current() as IFileStat; + let score = fuzzyScore(pattern, element.name, undefined, true); + this._scores.set(element.resource.toString(), score); + if (!topScore || score && topScore[0] < score[0]) { + topScore = score; + topElement = element; + } + } + return topElement; + } +} + +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 _getInitialSelection(tree: ITree, input: BreadcrumbElement): any { + let { uri } = (input as FileElement); + let nav = tree.getNavigator(); + while (nav.next()) { + if (isEqual(uri, (nav.current() as IFileStat).resource)) { + return nav.current(); + } + } + return undefined; + } + + protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration { + // 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(new FileElement(stat.resource, true)); + } + } +} +//#endregion + +//#region - Symbols + +class HighlightingOutlineRenderer extends OutlineRenderer implements IHighlightingRenderer { + + updateHighlights(tree: ITree, pattern: string): any { + let model = OutlineModel.get(tree.getInput()); + return model.updateMatches(pattern); + } +} + +export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { + + protected _getInput(input: BreadcrumbElement): any { + let element = input as TreeElement; + let model = OutlineModel.get(element); + model.updateMatches(''); + return model; + } + + protected _getInitialSelection(_tree: ITree, input: BreadcrumbElement): any { + return input instanceof OutlineModel ? undefined : input; + } + + protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration { + config.dataSource = this._instantiationService.createInstance(OutlineDataSource); + config.renderer = this._instantiationService.createInstance(HighlightingOutlineRenderer); + 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); + } + } +} + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index f633777987f..bd435ff853f 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 @@ -536,3 +536,138 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorComman MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeSavedEditors', "Close Saved Editors in Group"), category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeOtherEditors', "Close Other Editors in Group"), category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: nls.localize('closeRightEditors', "Close Editors to the Right in Group"), category } }); + +// File menu +MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: '1_editor', + command: { + id: ReopenClosedEditorAction.ID, + title: nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: 'z_clear', + command: { + id: ClearRecentFilesAction.ID, + title: nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened") + }, + order: 1 +}); + +// Layout menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_appearance', + title: nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout"), + submenu: MenuId.MenubarLayoutMenu, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_UP, + title: nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_DOWN, + title: nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_LEFT, + title: nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_RIGHT, + title: nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutSingleAction.ID, + title: nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoColumnsAction.ID, + title: nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutThreeColumnsAction.ID, + title: nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoRowsAction.ID, + title: nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows") + }, + order: 5 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutThreeRowsAction.ID, + title: nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows") + }, + order: 6 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoByTwoGridAction.ID, + title: nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)") + }, + order: 7 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoRowsRightAction.ID, + title: nls.localize({ key: 'miTwoRowsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two R&&ows Right") + }, + order: 8 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoColumnsBottomAction.ID, + title: nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom") + }, + order: 9 +}); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 1e4336156a6..248fa7871f5 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -117,6 +117,7 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito isEmpty(): boolean; setActive(isActive: boolean): void; setLabel(label: string): void; + relayout(): void; shutdown(): void; } @@ -159,4 +160,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/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 23253584873..c55b73e43ca 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -34,7 +34,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; -import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, EDITOR_TITLE_HEIGHT, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { join } from 'vs/base/common/paths'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -337,8 +337,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { target = (e as GestureEvent).initialTarget as HTMLElement; } - if (findParentWithClass(target, 'monaco-action-bar', this.titleContainer)) { - return; // not when clicking on actions + if (findParentWithClass(target, 'monaco-action-bar', this.titleContainer) || + findParentWithClass(target, 'monaco-breadcrumb-item', this.titleContainer) + ) { + return; // not when clicking on actions or breadcrumbs } // timeout to keep focus in editor after mouse up @@ -956,7 +958,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.doCloseInactiveEditor(editor); } - // Forward to title control + // Forward to title control & breadcrumbs this.titleAreaControl.closeEditor(editor); } @@ -1344,8 +1346,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.dimension = new Dimension(width, height); // 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.titleAreaControl.layout(new Dimension(this.dimension.width, this.titleAreaControl.getPreferredHeight())); + this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - this.titleAreaControl.getPreferredHeight())); + } + + relayout(): void { + if (this.dimension) { + const { width, height } = this.dimension; + this.layout(width, height); + } } toJSON(): ISerializedEditorGroup { 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/breadcrumbscontrol.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css new file mode 100644 index 00000000000..7b80fb89d06 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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 .breadcrumbs-control.hidden { + display: none; +} + +.monaco-workbench>.part.editor>.content .editor-group-container:not(.active) .breadcrumbs-control { + opacity: .8; +} + +/* todo@joh move somewhere else */ + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree>.input { + padding: 5px 9px; + position: relative; + box-sizing: border-box; + height: 36px; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree>.tree { + height: calc(100% - 36px); +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree.inactive>.input { + display: none; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree.inactive>.tree { + height: 100%; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree .monaco-highlighted-label .highlight{ + font-weight: bold; +} diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index e7abc5b7357..37f00da2fdc 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -44,10 +44,15 @@ .monaco-workbench > .part.editor > .content .editor-group-container > .title { position: relative; - height: 35px; display: flex; + flex-wrap: nowrap; box-sizing: border-box; overflow: hidden; + justify-content: space-between; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs { + flex-wrap: wrap; } .monaco-workbench > .part.editor > .content .editor-group-container > .title.title-border-bottom::after { @@ -112,4 +117,4 @@ .monaco-workbench > .part.editor > .content .grid-view-container { width: 100%; height: 100%; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 7a2683b4f03..6020090acfc 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -3,6 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench > .part.editor > .content .editor-group-container > .title > .label-container { + display: flex; + justify-content: flex-start; + align-items: center; + overflow: hidden; + flex: auto; +} + /* Title Label */ .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label { @@ -13,10 +21,51 @@ padding-left: 20px; } +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs.title-label { + flex: none; +} + .monaco-workbench > .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ } +/* Breadcrumbs */ + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control { + flex: 1 50%; + overflow: hidden; + padding: 0 6px; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control .monaco-breadcrumb-item { + font-size: 0.9em; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control.preview .monaco-breadcrumb-item { + font-style: italic; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control .monaco-breadcrumb-item::before { + content: '/'; + opacity: 1; + height: inherit; + width: inherit; + background-image: none; +} + +.monaco-workbench.windows > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control .monaco-breadcrumb-item::before { + content: '\\'; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control.relative-path .monaco-breadcrumb-item:nth-child(2)::before { + /* relative path -> hide first seperator */ + display: none; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .no-tabs-breadcrumbs.breadcrumbs-control .monaco-breadcrumb-item:last-child { + padding-right: 4px; /* does not have trailing separator*/ +} + /* Title Actions */ .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-actions { display: flex; @@ -26,4 +75,4 @@ .monaco-workbench > .part.editor > .content .editor-group-container.active > .title .title-actions { opacity: 1; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 2dc523ad8aa..74218a0d7a9 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -252,4 +252,30 @@ cursor: default; flex: initial; padding-left: 4px; -} \ No newline at end of file + height: 35px; +} + +/* Breadcrumbs */ + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { + flex: 1 100%; + height: 25px; + cursor: default; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label::before { + height: 18px; /* tweak the icon size of the editor labels when icons are enabled */ +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { + max-width: 260px; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { + padding-right: 8px; +} + +/* .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:not(:last-child):not(:hover):not(.focused):not(.file) { + min-width: 33px; +} */ + diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index da2df0dfeca..41e78a153f2 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -21,7 +21,7 @@ .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .monaco-icon-label::before, .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label a, .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label span, +.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label h2, .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label span { cursor: pointer; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 0d85263c31c..e4cd8fb91df 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -11,8 +11,8 @@ import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor import { ResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { addDisposableListener, EventType, addClass, EventHelper, removeClass } from 'vs/base/browser/dom'; -import { IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { addDisposableListener, EventType, addClass, EventHelper, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { IEditorPartOptions, EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; import { IAction } from 'vs/base/common/actions'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -31,10 +31,17 @@ export class NoTabsTitleControl extends TitleControl { // Gesture Support Gesture.addTarget(this.titleContainer); + const labelContainer = document.createElement('div'); + addClass(labelContainer, 'label-container'); + this.titleContainer.appendChild(labelContainer); + // Editor Label - this.editorLabel = this._register(this.instantiationService.createInstance(ResourceLabel, this.titleContainer, void 0)); + this.editorLabel = this._register(this.instantiationService.createInstance(ResourceLabel, labelContainer, void 0)); this._register(this.editorLabel.onClick(e => this.onTitleLabelClick(e))); + // Breadcrumbs + this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, extraClasses: ['no-tabs-breadcrumbs'] }); + // Right Actions Container const actionsContainer = document.createElement('div'); addClass(actionsContainer, 'title-actions'); @@ -84,6 +91,10 @@ export class NoTabsTitleControl extends TitleControl { } } + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT; + } + openEditor(editor: IEditorInput): void { this.ifActiveEditorChanged(() => this.redraw()); } @@ -156,6 +167,19 @@ export class NoTabsTitleControl extends TitleControl { const editor = this.group.activeEditor; this.lastRenderedActiveEditor = editor; + const isEditorPinned = this.group.isPinned(this.group.activeEditor); + const isGroupActive = this.accessor.activeGroup === this.group; + + // Update Breadcrumbs + if (this.breadcrumbsControl) { + if (isGroupActive) { + this.breadcrumbsControl.update(); + toggleClass(this.breadcrumbsControl.domNode, 'preview', !isEditorPinned); + } else { + this.breadcrumbsControl.hide(); + } + } + // Clear if there is no editor if (!editor) { removeClass(this.titleContainer, 'dirty'); @@ -165,9 +189,6 @@ export class NoTabsTitleControl extends TitleControl { // Otherwise render it else { - const isEditorPinned = this.group.isPinned(this.group.activeEditor); - const isGroupActive = this.accessor.activeGroup === this.group; - // Dirty state this.updateEditorDirty(editor); @@ -177,7 +198,9 @@ export class NoTabsTitleControl extends TitleControl { const { labelFormat } = this.accessor.partOptions; let description: string; - if (labelFormat === 'default' && !isGroupActive) { + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + description = ''; // hide description when showing breadcrumbs + } else if (labelFormat === 'default' && !isGroupActive) { description = ''; // hide description when group is not active and style is 'default' } else { description = editor.getDescription(this.getVerbosity(labelFormat)) || ''; @@ -188,7 +211,7 @@ export class NoTabsTitleControl extends TitleControl { title = ''; // dont repeat what is already shown } - this.editorLabel.setLabel({ name, description, resource }, { title, italic: !isEditorPinned, extraClasses: ['title-label'] }); + this.editorLabel.setLabel({ name, description, resource }, { title, italic: !isEditorPinned, extraClasses: ['no-tabs', 'title-label'] }); if (isGroupActive) { this.editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND); } else { diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index b262ab1f852..b38e32f681a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -54,7 +54,7 @@ export class SideBySideEditor extends BaseEditor { private onDidCreateEditors = this._register(new Emitter<{ width: number; height: number; }>()); private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; }>()); - get onDidSizeConstraintsChange(): Event<{ width: number; height: number; }> { return anyEvent(this.onDidCreateEditors.event, this._onDidSizeConstraintsChange.event); } + readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }> = anyEvent(this.onDidCreateEditors.event, this._onDidSizeConstraintsChange.event); constructor( @ITelemetryService telemetryService: ITelemetryService, diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 8934bb9925f..605ac959776 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -39,6 +39,8 @@ import { addClass, addDisposableListener, hasClass, EventType, EventHelper, remo import { localize } from 'vs/nls'; import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; interface IEditorInputLabel { name: string; @@ -78,9 +80,10 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickOpenService quickOpenService: IQuickOpenService, @IThemeService themeService: IThemeService, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @IConfigurationService configurationService: IConfigurationService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService); } protected create(parent: HTMLElement): void { @@ -108,6 +111,12 @@ export class TabsTitleControl extends TitleControl { // Close Action this.closeOneEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); + + // Breadcrumbs + const breadcrumbsContainer = document.createElement('div'); + addClass(breadcrumbsContainer, 'tabs-breadcrumbs'); + this.titleContainer.appendChild(breadcrumbsContainer); + this.createBreadcrumbsControl(breadcrumbsContainer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, extraClasses: [] }); } private createScrollbar(): void { @@ -128,6 +137,14 @@ export class TabsTitleControl extends TitleControl { this.titleContainer.appendChild(this.scrollbar.getDomNode()); } + private updateBreadcrumbsControl(): void { + if (this.breadcrumbsControl && this.breadcrumbsControl.update()) { + // relayout when we have a breadcrumbs and when update changed + // its hidden-status + this.group.relayout(); + } + } + private registerContainerListeners(): void { // Group dragging @@ -240,6 +257,9 @@ export class TabsTitleControl extends TitleControl { // Redraw all tabs this.redraw(); + + // Update Breadcrumbs + this.updateBreadcrumbsControl(); } closeEditor(editor: IEditorInput): void { @@ -287,6 +307,9 @@ export class TabsTitleControl extends TitleControl { this.clearEditorActionsToolbar(); } + + // Update Breadcrumbs + this.updateBreadcrumbsControl(); } moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void { @@ -910,6 +933,11 @@ export class TabsTitleControl extends TitleControl { return; } + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT }); + this.scrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + } + const visibleContainerWidth = this.tabsContainer.offsetWidth; const totalContainerWidth = this.tabsContainer.scrollWidth; @@ -1198,4 +1226,4 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { `); } } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8ddd3329287..3a123c9c218 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -5,38 +5,41 @@ 'use strict'; -import 'vs/css!./media/titlecontrol'; -import { localize } from 'vs/nls'; -import { prepareActions } from 'vs/workbench/browser/actions'; -import { IAction, Action, IRunEvent } from 'vs/base/common/actions'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import * as arrays from 'vs/base/common/arrays'; -import { toResource, IEditorCommandsContext, IEditorInput, EditorCommandsContextActionRunner } from 'vs/workbench/common/editor'; -import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { createActionItem, fillInContextMenuActions, fillInActionBarActions } from 'vs/platform/actions/browser/menuItemActionItem'; -import { IMenuService, MenuId, IMenu, ExecuteCommandAction } from 'vs/platform/actions/common/actions'; -import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { Themable } from 'vs/workbench/common/theme'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { LocalSelectionTransfer, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; import { applyDragImage } from 'vs/base/browser/dnd'; +import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { ActionsOrientation, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { Action, IAction, IRunEvent } from 'vs/base/common/actions'; +import * as arrays from 'vs/base/common/arrays'; +import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import 'vs/css!./media/titlecontrol'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { createActionItem, fillInActionBarActions, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { ExecuteCommandAction, IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { prepareActions } from 'vs/workbench/browser/actions'; +import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; +import { EDITOR_TITLE_HEIGHT, IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource } from 'vs/workbench/common/editor'; +import { ResourceContextKey } from 'vs/workbench/common/resources'; +import { Themable } from 'vs/workbench/common/theme'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export interface IToolbarActions { primary: IAction[]; @@ -48,6 +51,8 @@ export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); protected readonly editorTransfer = LocalSelectionTransfer.getInstance(); + protected breadcrumbsControl: BreadcrumbsControl; + private currentPrimaryEditorActionIds: string[] = []; private currentSecondaryEditorActionIds: string[] = []; protected editorActionsToolbar: ToolBar; @@ -72,7 +77,8 @@ export abstract class TitleControl extends Themable { @IMenuService private menuService: IMenuService, @IQuickOpenService protected quickOpenService: IQuickOpenService, @IThemeService themeService: IThemeService, - @IExtensionService private extensionService: IExtensionService + @IExtensionService private extensionService: IExtensionService, + @IConfigurationService protected configurationService: IConfigurationService ) { super(themeService); @@ -89,6 +95,24 @@ export abstract class TitleControl extends Themable { protected abstract create(parent: HTMLElement): void; + protected createBreadcrumbsControl(container: HTMLElement, options: IBreadcrumbsControlOptions): void { + const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService)); + config.onDidChange(value => { + if (!value && this.breadcrumbsControl) { + this.breadcrumbsControl.dispose(); + this.breadcrumbsControl = undefined; + this.group.relayout(); + } else if (value && !this.breadcrumbsControl) { + this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); + this.breadcrumbsControl.update(); + this.group.relayout(); + } + }); + if (config.value) { + this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); + } + } + protected createEditorActionsToolBar(container: HTMLElement): void { const context = { groupId: this.group.id } as IEditorCommandsContext; @@ -327,9 +351,18 @@ export abstract class TitleControl extends Themable { layout(dimension: Dimension): void { // Optionally implemented in subclasses + + if (this.breadcrumbsControl) { + this.breadcrumbsControl.layout(undefined); + } + } + + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT + (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden() ? BreadcrumbsControl.HEIGHT : 0); } dispose(): void { + this.breadcrumbsControl = dispose(this.breadcrumbsControl); this.editorToolBarMenuDisposables = dispose(this.editorToolBarMenuDisposables); super.dispose(); diff --git a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css index 62b8c849063..5b52a860a93 100644 --- a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css +++ b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css @@ -6,7 +6,6 @@ .monaco-workbench > .part.menubar { display: flex; position: absolute; - font-size: 12px; box-sizing: border-box; padding-left: 35px; padding-right: 138px; @@ -32,16 +31,6 @@ zoom: 1; } -.monaco-workbench > .part.menubar > .menubar-menu-button.open, -.monaco-workbench > .part.menubar > .menubar-menu-button:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -.monaco-workbench > .part.menubar.light > .menubar-menu-button.open, -.monaco-workbench > .part.menubar.light > .menubar-menu-button:hover { - background-color: rgba(0, 0, 0, 0.1); -} - .menubar-menu-items-holder { position: absolute; left: 0px; @@ -49,9 +38,9 @@ } .menubar-menu-items-holder.monaco-menu-container { - box-shadow: 0 2px 8px #A8A8A8; + box-shadow: 0 2px 4px; } .vs-dark .menubar-menu-items-holder.monaco-menu-container { - box-shadow: 0 2px 8px #000; + box-shadow: 0 2px 4px; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts index 8d877ddba77..6c2e0b4adfc 100644 --- a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts +++ b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts @@ -7,186 +7,17 @@ import * as nls from 'vs/nls'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { isMacintosh } from 'vs/base/common/platform'; -recentMenuRegistration(); -fileMenuRegistration(); editMenuRegistration(); selectionMenuRegistration(); -viewMenuRegistration(); -layoutMenuRegistration(); goMenuRegistration(); -debugMenuRegistration(); -tasksMenuRegistration(); -terminalMenuRegistration(); if (isMacintosh) { windowMenuRegistration(); } -preferencesMenuRegistration(); helpMenuRegistration(); -// Menu registration - File Menu -function fileMenuRegistration() { - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '1_new', - command: { - id: 'workbench.action.files.newUntitledFile', - title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '1_new', - command: { - id: 'workbench.action.newWindow', - title: nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '2_open', - command: { - id: 'workbench.action.files.openFile', - title: nls.localize({ key: 'miOpenFile', comment: ['&& denotes a mnemonic'] }, "&&Open File...") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '2_open', - command: { - id: 'workbench.action.files.openFolder', - title: nls.localize({ key: 'miOpenFolder', comment: ['&& denotes a mnemonic'] }, "Open &&Folder...") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '2_open', - command: { - id: 'workbench.action.openWorkspace', - title: nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - title: nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"), - submenu: MenuId.MenubarRecentMenu, - group: '2_open', - order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '3_workspace', - command: { - id: 'workbench.action.addRootFolder', - title: nls.localize({ key: 'miAddFolderToWorkspace', comment: ['&& denotes a mnemonic'] }, "A&&dd Folder to Workspace...") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '3_workspace', - command: { - id: 'workbench.action.saveWorkspaceAs', - title: nls.localize('miSaveWorkspaceAs', "Save Workspace As...") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '4_save', - command: { - id: 'workbench.action.files.save', - title: nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save"), - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '4_save', - command: { - id: 'workbench.action.files.saveAs', - title: nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As...") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '4_save', - command: { - id: 'workbench.action.files.saveAll', - title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '5_autosave', - command: { - id: 'workbench.action.toggleAutoSave', - title: nls.localize('miAutoSave', "Auto Save") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - title: nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences"), - submenu: MenuId.MenubarPreferencesMenu, - group: '5_autosave', - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: '', - title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: 'workbench.action.closeActiveEditor', - title: nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "&&Close Editor") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: 'workbench.action.closeFolder', - title: nls.localize({ key: 'miCloseFolder', comment: ['&& denotes a mnemonic'] }, "Close &&Folder") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: 'workbench.action.closeWindow', - title: nls.localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window") - }, - order: 4 - }); - - if (!isMacintosh) { - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: 'z_Exit', - command: { - id: 'workbench.action.quit', - title: nls.localize({ key: 'miExit', comment: ['&& denotes a mnemonic'] }, "E&&xit") - }, - order: 1 - }); - } -} +// Menu registration function editMenuRegistration() { MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { @@ -272,9 +103,6 @@ function editMenuRegistration() { }); - /// - - MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { group: '5_insert', command: { @@ -440,432 +268,6 @@ function selectionMenuRegistration() { }); } -function recentMenuRegistration() { - // Editor - MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { - group: '1_editor', - command: { - id: 'workbench.action.reopenClosedEditor', - title: nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor") - }, - order: 1 - }); - - // More - MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { - group: 'y_more', - command: { - id: 'workbench.action.openRecent', - title: nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More...") - }, - order: 1 - }); - - // Clear - MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { - group: 'z_clear', - command: { - id: 'workbench.action.clearRecentFiles', - title: nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened") - }, - order: 1 - }); - -} - -function viewMenuRegistration() { - - // Command Palette - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '1_open', - command: { - id: 'workbench.action.showCommands', - title: nls.localize({ key: 'miCommandPalette', comment: ['&& denotes a mnemonic'] }, "&&Command Palette...") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '1_open', - command: { - id: 'workbench.action.openView', - title: nls.localize({ key: 'miOpenView', comment: ['&& denotes a mnemonic'] }, "&&Open View...") - }, - order: 2 - }); - - // Viewlets - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', - command: { - id: 'workbench.view.explorer', - title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', - command: { - id: 'workbench.view.search', - title: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', - command: { - id: 'workbench.view.scm', - title: nls.localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', - command: { - id: 'workbench.view.debug', - title: nls.localize({ key: 'miViewDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug") - }, - order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '2_views', - command: { - id: 'workbench.view.extensions', - title: nls.localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") - }, - order: 5 - }); - - // Panels - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', - command: { - id: 'workbench.action.output.toggleOutput', - title: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', - command: { - id: 'workbench.debug.action.toggleRepl', - title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', - command: { - id: 'workbench.action.terminal.toggleTerminal', - title: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Integrated Terminal") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_panels', - command: { - id: 'workbench.actions.view.problems', - title: nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") - }, - order: 4 - }); - - // Toggle View - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleFullScreen', - title: nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleZenMode', - title: nls.localize('miToggleZenMode', "Toggle Zen Mode") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleCenteredLayout', - title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_toggle_view', - command: { - id: 'workbench.action.toggleMenuBar', - title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") - }, - order: 4 - }); - - // TODO: Editor Layout Submenu - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - title: nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout"), - submenu: MenuId.MenubarLayoutMenu, - group: '5_layout', - order: 1 - }); - - - // Workbench Layout - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleSidebarVisibility', - title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleSidebarPosition', - title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleStatusbarVisibility', - title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.toggleActivityBarVisibility', - title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") - }, - order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '6_workbench_layout', - command: { - id: 'workbench.action.togglePanel', - title: nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel") - }, - order: 5 - }); - - // Toggle Editor Settings - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', - command: { - id: 'workbench.action.toggleWordWrap', - title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', - command: { - id: 'workbench.action.toggleMinimap', - title: nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', - command: { - id: 'workbench.action.toggleRenderWhitespace', - title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '8_editor', - command: { - id: 'workbench.action.toggleRenderControlCharacters', - title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters") - }, - order: 4 - }); - - // Zoom - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '9_zoom', - command: { - id: 'workbench.action.zoomIn', - title: nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '9_zoom', - command: { - id: 'workbench.action.zoomOut', - title: nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '9_zoom', - command: { - id: 'workbench.action.zoomReset', - title: nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") - }, - order: 3 - }); -} - -function layoutMenuRegistration() { - // Split - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '1_split', - command: { - id: 'workbench.action.splitEditorUp', - title: nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '1_split', - command: { - id: 'workbench.action.splitEditorDown', - title: nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '1_split', - command: { - id: 'workbench.action.splitEditorLeft', - title: nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '1_split', - command: { - id: 'workbench.action.splitEditorRight', - title: nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right") - }, - order: 4 - }); - - // Layouts - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutSingle', - title: nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutCentered', - title: nls.localize({ key: 'miCenteredEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutTwoColumns', - title: nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutThreeColumns', - title: nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns") - }, - order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutTwoRows', - title: nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows") - }, - order: 5 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutThreeRows', - title: nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows") - }, - order: 6 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutTwoByTwoGrid', - title: nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)") - }, - order: 7 - }); - - 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") - }, - order: 8 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: '2_layouts', - command: { - id: 'workbench.action.editorLayoutTwoColumnsBottom', - title: nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom") - }, - order: 9 - }); - - // Flip - MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: 'z_flip', - command: { - id: 'workbench.action.toggleEditorGroupLayout', - title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") - }, - order: 1 - }); - -} function goMenuRegistration() { // Forward/Back @@ -1044,305 +446,10 @@ function goMenuRegistration() { }); } -function debugMenuRegistration() { - // Start/Stop Debug - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: 'workbench.action.debug.start', - title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: 'workbench.action.debug.run', - title: nls.localize({ key: 'miStartWithoutDebugging', comment: ['&& denotes a mnemonic'] }, "Start &&Without Debugging") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: 'workbench.action.debug.stop', - title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '1_debug', - command: { - id: 'workbench.action.debug.restart', - title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging") - }, - order: 4 - }); - - // Configuration - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: 'workbench.action.debug.configure', - title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: 'debug.addConfiguration', - title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "Add Configuration...") - }, - order: 2 - }); - - // Step Commands - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: 'workbench.action.debug.stepOver', - title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: 'workbench.action.debug.stepInto', - title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: 'workbench.action.debug.stepOut', - title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '3_step', - command: { - id: 'workbench.action.debug.continue', - title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue") - }, - order: 4 - }); - - // New Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: 'editor.debug.action.toggleBreakpoint', - title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: 'editor.debug.action.conditionalBreakpoint', - title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Conditional Breakpoint...") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: 'editor.debug.action.toggleInlineBreakpoint', - title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle Inline Breakp&&oint") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: 'workbench.debug.viewlet.action.addFunctionBreakpointAction', - title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Function Breakpoint...") - }, - order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: 'editor.debug.action.toggleLogPoint', - title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Logpoint...") - }, - order: 5 - }); - - // Modify Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: 'workbench.debug.viewlet.action.enableAllBreakpoints', - title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Enable All Breakpoints") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: 'workbench.debug.viewlet.action.disableAllBreakpoints', - title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: 'workbench.debug.viewlet.action.removeAllBreakpoints', - title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") - }, - order: 3 - }); - -} - -function tasksMenuRegistration() { - // Run Tasks - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '1_run', - command: { - id: 'workbench.action.tasks.runTask', - title: nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task...") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '1_run', - command: { - id: 'workbench.action.tasks.build', - title: nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task...") - }, - order: 2 - }); - - // Manage Tasks - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '2_manage', - command: { - id: 'workbench.action.tasks.showTasks', - title: nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks...") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '2_manage', - command: { - id: 'workbench.action.tasks.restartTask', - title: nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task...") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '2_manage', - command: { - id: 'workbench.action.tasks.terminate', - title: nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task...") - }, - order: 3 - }); - - // Configure Tasks - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '3_configure', - command: { - id: 'workbench.action.tasks.configureTaskRunner', - title: nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks...") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { - group: '3_configure', - command: { - id: 'workbench.action.tasks.configureDefaultBuildTask', - title: nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task...") - }, - order: 2 - }); - -} - function windowMenuRegistration() { } -function preferencesMenuRegistration() { - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: 'workbench.action.openSettings', - title: nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '2_keybindings', - command: { - id: 'workbench.action.openGlobalKeybindings', - title: nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '2_keybindings', - command: { - id: 'workbench.extensions.action.showRecommendedKeymapExtensions', - title: nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '3_snippets', - command: { - id: 'workbench.action.openSnippets', - title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '4_themes', - command: { - id: 'workbench.action.selectTheme', - title: nls.localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '4_themes', - command: { - id: 'workbench.action.selectIconTheme', - title: nls.localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme") - }, - order: 2 - }); -} - function helpMenuRegistration() { // Welcome MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { @@ -1494,102 +601,3 @@ function helpMenuRegistration() { order: 1 }); } - -function terminalMenuRegistration() { - - // Manage - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '1_manage', - command: { - id: 'workbench.action.terminal.new', - title: nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '1_manage', - command: { - id: 'workbench.action.terminal.split', - title: nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '1_manage', - command: { - id: 'workbench.action.terminal.kill', - title: nls.localize({ key: 'miKillTerminal', comment: ['&& denotes a mnemonic'] }, "&&Kill Terminal") - }, - order: 3 - }); - - // Run - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '2_run', - command: { - id: 'workbench.action.terminal.clear', - title: nls.localize({ key: 'miClear', comment: ['&& denotes a mnemonic'] }, "&&Clear") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '2_run', - command: { - id: 'workbench.action.terminal.runActiveFile', - title: nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '2_run', - command: { - id: 'workbench.action.terminal.runSelectedFile', - title: nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text") - }, - order: 3 - }); - - // Selection - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '3_selection', - command: { - id: 'workbench.action.terminal.scrollToPreviousCommand', - title: nls.localize({ key: 'miScrollToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Previous Command") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '3_selection', - command: { - id: 'workbench.action.terminal.scrollToNextCommand', - title: nls.localize({ key: 'miScrollToNextCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Next Command") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '3_selection', - command: { - id: 'workbench.action.terminal.selectToPreviousCommand', - title: nls.localize({ key: 'miSelectToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Select To Previous Command") - }, - order: 3 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { - group: '3_selection', - command: { - id: 'workbench.action.terminal.selectToNextCommand', - title: nls.localize({ key: 'miSelectToNextCommand', comment: ['&& denotes a mnemonic'] }, "Select To Next Command") - }, - order: 4 - }); -} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/menubar/menubarPart.ts b/src/vs/workbench/browser/parts/menubar/menubarPart.ts index 31c84306e65..018fded98cd 100644 --- a/src/vs/workbench/browser/parts/menubar/menubarPart.ts +++ b/src/vs/workbench/browser/parts/menubar/menubarPart.ts @@ -12,35 +12,44 @@ import * as browser from 'vs/base/browser/browser'; import { Part } from 'vs/workbench/browser/part'; import { IMenubarService, IMenubarMenu, IMenubarMenuItemAction, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IWindowService, MenuBarVisibility, IWindowsService } from 'vs/platform/windows/common/windows'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ActionRunner, IActionRunner, IAction, Action } from 'vs/base/common/actions'; import { Builder, $ } from 'vs/base/browser/builder'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { EventType, Dimension, toggleClass } from 'vs/base/browser/dom'; -import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; +import { EventType, Dimension } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { Menu, IMenuOptions, SubmenuAction } from 'vs/base/browser/ui/menu/menu'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { Color } from 'vs/base/common/color'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { domEvent } from 'vs/base/browser/event'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, getWorkspaceLabel } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { getPathLabel } from 'vs/base/common/labels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_BORDER, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, MENU_BACKGROUND, MENU_FOREGROUND, MENU_SELECTION_BACKGROUND, MENU_SELECTION_FOREGROUND, MENU_SELECTION_BORDER } from 'vs/workbench/common/theme'; +import URI from 'vs/base/common/uri'; interface CustomMenu { title: string; + buttonElement: Builder; titleElement: Builder; actions?: IAction[]; } +enum MenubarState { + HIDDEN, + VISIBLE, + FOCUSED, + OPEN +} + export class MenubarPart extends Part { private keys = [ @@ -74,7 +83,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") @@ -82,17 +91,21 @@ export class MenubarPart extends Part { private focusedMenu: { index: number; - holder: Builder; - widget: Menu; + holder?: Builder; + widget?: Menu; }; private customMenus: CustomMenu[]; + private menuUpdater: RunOnceScheduler; private actionRunner: IActionRunner; + private focusToReturn: Builder; private container: Builder; private recentlyOpened: IRecentlyOpened; + private updatePending: boolean; private _modifierKeyStatus: IModifierKeyStatus; - private _isFocused: boolean; + private _focusState: MenubarState; + private _onVisibilityChange: Emitter; private initialSizing: { @@ -135,11 +148,11 @@ export class MenubarPart extends Part { this.topLevelMenus['Window'] = this._register(this.menuService.createMenu(MenuId.MenubarWindowMenu, this.contextKeyService)); } + this.menuUpdater = this._register(new RunOnceScheduler(() => this.doSetupMenubar(), 0)); + this.actionRunner = this._register(new ActionRunner()); this._register(this.actionRunner.onDidBeforeRun(() => { - if (this.focusedMenu && this.focusedMenu.holder) { - this.focusedMenu.holder.hide(); - } + this.setUnfocusedState(); })); this._onVisibilityChange = this._register(new Emitter()); @@ -148,10 +161,10 @@ export class MenubarPart extends Part { for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar())); } - this.setupMenubar(); + this.doSetupMenubar(); } - this.isFocused = false; + this._focusState = MenubarState.HIDDEN; this.windowService.getRecentlyOpened().then((recentlyOpened) => { this.recentlyOpened = recentlyOpened; @@ -207,30 +220,132 @@ export class MenubarPart extends Part { return this.configurationService.getValue('window.titleBarStyle'); } - private get isFocused(): boolean { - return this._isFocused; + private get focusState(): MenubarState { + return this._focusState; } - private set isFocused(value: boolean) { - this._isFocused = value; + private set focusState(value: MenubarState) { + if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) { + // Losing focus, update the menu if needed - if (!this._isFocused && this.currentMenubarVisibility === 'toggle') { - if (this.container) { - this.hideMenubar(); + if (this.updatePending) { + this.menuUpdater.schedule(); + this.updatePending = false; } } + + if (value === this._focusState) { + return; + } + + switch (value) { + case MenubarState.HIDDEN: + if (this.isVisible) { + this.hideMenubar(); + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + } + + if (this.isFocused) { + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.domFocus(); + this.focusToReturn = null; + } + } + + + break; + case MenubarState.VISIBLE: + if (!this.isVisible) { + this.showMenubar(); + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + } + + if (this.isFocused) { + if (this.focusedMenu) { + this.customMenus[this.focusedMenu.index].buttonElement.domBlur(); + } + + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.domFocus(); + this.focusToReturn = null; + } + } + + break; + case MenubarState.FOCUSED: + if (!this.isVisible) { + this.showMenubar(); + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + } + + if (this.focusedMenu) { + this.customMenus[this.focusedMenu.index].buttonElement.domFocus(); + } + break; + case MenubarState.OPEN: + if (!this.isVisible) { + this.showMenubar(); + } + + if (this.focusedMenu) { + this.showCustomMenu(this.focusedMenu.index); + } + break; + } + + this._focusState = value; + } + + private get isVisible(): boolean { + return this.focusState >= MenubarState.VISIBLE; + } + + private get isFocused(): boolean { + return this.focusState >= MenubarState.FOCUSED; + } + + private get isOpen(): boolean { + return this.focusState >= MenubarState.OPEN; } private onDidChangeFullscreen(): void { this.updateStyles(); } + private onDidChangeWindowFocus(hasFocus: boolean): void { + if (this.container) { + if (hasFocus) { + this.container.removeClass('inactive'); + } else { + this.container.addClass('inactive'); + this.setUnfocusedState(); + } + } + } + private onConfigurationUpdated(event: IConfigurationChangeEvent): void { if (this.keys.some(key => event.affectsConfiguration(key))) { this.setupMenubar(); } } + private setUnfocusedState(): void { + this.focusState = this.currentMenubarVisibility === 'toggle' ? MenubarState.HIDDEN : MenubarState.VISIBLE; + } + private hideMenubar(): void { this._onVisibilityChange.fire(new Dimension(0, 0)); this.container.style('visibility', 'hidden'); @@ -241,26 +356,35 @@ export class MenubarPart extends Part { this.container.style('visibility', null); } - private onModifierKeyToggled(modiferKeyStatus: IModifierKeyStatus): void { + private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { + this._modifierKeyStatus = modifierKeyStatus; + const altKeyAlone = modifierKeyStatus.lastKeyPressed === 'alt' && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + if (this.currentMenubarVisibility === 'toggle') { - const altKeyPressed = (!this._modifierKeyStatus || !this._modifierKeyStatus.altKey) && modiferKeyStatus.altKey; - if (altKeyPressed && !modiferKeyStatus.ctrlKey && !modiferKeyStatus.shiftKey) { - this.showMenubar(); - } else if (!this.isFocused) { - this.hideMenubar(); + if (altKeyAlone) { + if (!this.isVisible) { + this.focusState = MenubarState.VISIBLE; + } + } else if (!allModifiersReleased && !this.isFocused) { + this.focusState = MenubarState.HIDDEN; } } - this._modifierKeyStatus = modiferKeyStatus; + if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') { + if (!this.isFocused) { + this.focusedMenu = { index: 0 }; + this.focusState = MenubarState.FOCUSED; + } else if (!this.isOpen) { + this.setUnfocusedState(); + } + } if (this.currentEnableMenuBarMnemonics && this.customMenus) { this.customMenus.forEach(customMenu => { let child = customMenu.titleElement.child(); if (child) { - let grandChild = child.child(); - if (grandChild) { - grandChild.style('text-decoration', modiferKeyStatus.altKey ? 'underline' : null); - } + child.style('text-decoration', modifierKeyStatus.altKey ? 'underline' : null); } }); } @@ -274,24 +398,35 @@ export class MenubarPart extends Part { } private registerListeners(): void { - this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); - // Update when config changes this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); // Listen to update service // this.updateService.onStateChange(() => this.setupMenubar()); + // Listen for context changes + this._register(this.contextKeyService.onDidChangeContext(() => this.setupMenubar())); + // Listen for changes in recently opened menu this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); // Listen to keybindings change this._register(this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar())); - this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); + // These listeners only apply when the custom menubar is being used + if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { + // Listen to fullscreen changes + this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); + + // Listen for alt key presses + this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); + + // Listen for window focus changes + this._register(this.windowService.onDidChangeFocus(e => this.onDidChangeWindowFocus(e))); + } } - private setupMenubar(): void { + private doSetupMenubar(): void { if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { this.setupCustomMenubar(); } else { @@ -299,6 +434,10 @@ export class MenubarPart extends Part { } } + private setupMenubar(): void { + this.menuUpdater.schedule(); + } + private setupNativeMenubar(): void { // TODO@sbatten: Remove once native menubar is ready if (isMacintosh && isWindows) { @@ -306,6 +445,11 @@ export class MenubarPart extends Part { } } + + private clearMnemonic(topLevelElement: HTMLElement): void { + topLevelElement.accessKey = null; + } + private registerMnemonic(topLevelElement: HTMLElement, mnemonic: string): void { topLevelElement.accessKey = mnemonic.toLocaleLowerCase(); } @@ -368,20 +512,23 @@ export class MenubarPart extends Part { private createOpenRecentMenuAction(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): IAction { let label: string; - let path: string; + let uri: URI; - if (isSingleFolderWorkspaceIdentifier(workspace) || typeof workspace === 'string') { - label = getPathLabel(workspace, this.environmentService); - path = workspace; - } else { + if (isSingleFolderWorkspaceIdentifier(workspace)) { label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); - path = workspace.configPath; + uri = workspace; + } else if (isWorkspaceIdentifier(workspace)) { + label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); + uri = URI.file(workspace.configPath); + } else { + label = getPathLabel(workspace, this.environmentService); + uri = URI.file(workspace); } return new Action(commandId, label, undefined, undefined, (event) => { const openInNewWindow = event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); - return this.windowService.openWindow([path], { + return this.windowService.openWindow([uri], { forceNewWindow: openInNewWindow, forceOpenWorkspaceAsFile: isFile }); @@ -428,10 +575,18 @@ export class MenubarPart extends Part { } private setupCustomMenubar(): void { - this.container.empty(); + // Don't update while using the menu + if (this.isFocused) { + this.updatePending = true; + return; + } + this.container.attr('role', 'menubar'); - this.customMenus = []; + const firstTimeSetup = this.customMenus === undefined; + if (firstTimeSetup) { + this.customMenus = []; + } let idx = 0; @@ -439,26 +594,34 @@ export class MenubarPart extends Part { const menu: IMenu = this.topLevelMenus[menuTitle]; let menuIndex = idx++; - let titleElement = $(this.container).div({ class: 'menubar-menu-button' }); + // Create the top level menu button element + if (firstTimeSetup) { + const buttonElement = $(this.container).div({ class: 'menubar-menu-button' }).attr({ 'role': 'menu', 'tabindex': 0 }); + buttonElement.attr('aria-label', this.topLevelTitles[menuTitle].replace(/&&(.)/g, '$1')); + + const titleElement = $(buttonElement).div({ class: 'menubar-menu-title', 'aria-hidden': true }); + + this.customMenus.push({ + title: menuTitle, + buttonElement: buttonElement, + titleElement: titleElement + }); + } + + // Update the button label to reflect mnemonics let displayTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, this.currentEnableMenuBarMnemonics ? '$1' : '$1'); - let legibleTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, '$1'); - $(titleElement).div({ class: 'menubar-menu-title', 'aria-hidden': true }).innerHtml(displayTitle); - - titleElement.attr('aria-label', legibleTitle); - titleElement.attr('role', 'menu'); + $(this.customMenus[menuIndex].titleElement).innerHtml(displayTitle); + // Clear and register mnemonics due to updated settings + this.clearMnemonic(this.customMenus[menuIndex].buttonElement.getHTMLElement()); if (this.currentEnableMenuBarMnemonics) { let mnemonic = (/&&(.)/g).exec(this.topLevelTitles[menuTitle]); if (mnemonic && mnemonic[1]) { - this.registerMnemonic(titleElement.getHTMLElement(), mnemonic[1]); + this.registerMnemonic(this.customMenus[menuIndex].buttonElement.getHTMLElement(), mnemonic[1]); } } - this.customMenus.push({ - title: menuTitle, - titleElement: titleElement - }); - + // Update the menu actions const updateActions = (menu: IMenu, target: IAction[]) => { target.splice(0); let groups = menu.getActions(); @@ -486,56 +649,118 @@ export class MenubarPart extends Part { }; 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, () => { - if (this._modifierKeyStatus && (this._modifierKeyStatus.shiftKey || this._modifierKeyStatus.ctrlKey)) { - return; // supress keyboard shortcuts that shouldn't conflict - } - - this.toggleCustomMenu(menuIndex); - this.isFocused = !this.isFocused; - }); - - this.customMenus[menuIndex].titleElement.on(EventType.MOUSE_ENTER, () => { - if (this.isFocused && !this.isCurrentMenu(menuIndex)) { - this.toggleCustomMenu(menuIndex); - } - }); - - this.customMenus[menuIndex].titleElement.on(EventType.MOUSE_LEAVE, () => { - if (!this.isFocused) { - this.cleanupCustomMenu(); - } - }); - - this.customMenus[menuIndex].titleElement.on(EventType.BLUR, () => { - this.cleanupCustomMenu(); - }); - } - - this.container.off(EventType.KEY_DOWN); - this.container.on(EventType.KEY_DOWN, (e) => { - let event = new StandardKeyboardEvent(e as KeyboardEvent); - let eventHandled = true; - - if (event.equals(KeyCode.LeftArrow) || (event.shiftKey && event.keyCode === KeyCode.Tab)) { - this.focusPrevious(); - } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Tab)) { - this.focusNext(); - } else { - eventHandled = false; + if (firstTimeSetup) { + this._register(menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions))); } - if (eventHandled) { - event.preventDefault(); - event.stopPropagation(); + updateActions(menu, this.customMenus[menuIndex].actions); + + if (firstTimeSetup) { + this.customMenus[menuIndex].buttonElement.on(EventType.KEY_UP, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.focusState = MenubarState.OPEN; + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + this.customMenus[menuIndex].buttonElement.on(EventType.CLICK, (e) => { + if (this._modifierKeyStatus && (this._modifierKeyStatus.shiftKey || this._modifierKeyStatus.ctrlKey)) { + return; // supress keyboard shortcuts that shouldn't conflict + } + + if (this.isOpen) { + if (this.isCurrentMenu(menuIndex)) { + this.setUnfocusedState(); + } else { + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex); + } + } else { + this.focusedMenu = { index: menuIndex }; + this.focusState = MenubarState.OPEN; + } + + e.preventDefault(); + e.stopPropagation(); + }); + + this.customMenus[menuIndex].buttonElement.on(EventType.MOUSE_ENTER, () => { + if (this.isOpen && !this.isCurrentMenu(menuIndex)) { + this.customMenus[menuIndex].buttonElement.domFocus(); + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex); + } else if (this.isFocused && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.customMenus[menuIndex].buttonElement.domFocus(); + } + }); + + } + } + + if (firstTimeSetup) { + this.container.on(EventType.KEY_DOWN, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if (event.equals(KeyCode.LeftArrow) || (event.shiftKey && event.keyCode === KeyCode.Tab)) { + this.focusPrevious(); + } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Tab)) { + this.focusNext(); + } else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) { + this.setUnfocusedState(); + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + this._register($(window).on(EventType.CLICK, () => { + // This click is outside the menubar so it counts as a focus out + if (this.isFocused) { + this.setUnfocusedState(); + } + })); + } + + this.container.on(EventType.FOCUS_IN, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.getHTMLElement().contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = $(event.relatedTarget as HTMLElement); + } + } + }); + + this.container.on(EventType.FOCUS_OUT, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.getHTMLElement().contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = null; + this.setUnfocusedState(); + } } }); } private focusPrevious(): void { + if (!this.focusedMenu) { return; } @@ -546,7 +771,13 @@ export class MenubarPart extends Part { return; } - this.toggleCustomMenu(newFocusedIndex); + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + this.customMenus[newFocusedIndex].buttonElement.domFocus(); + } } private focusNext(): void { @@ -560,7 +791,13 @@ export class MenubarPart extends Part { return; } - this.toggleCustomMenu(newFocusedIndex); + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + this.customMenus[newFocusedIndex].buttonElement.domFocus(); + } } private getMenubarMenus(): IMenubarData { @@ -619,33 +856,15 @@ export class MenubarPart extends Part { if (this.focusedMenu.widget) { this.focusedMenu.widget.dispose(); } + + this.focusedMenu = { index: this.focusedMenu.index }; } - - this.focusedMenu = null; } - public focusCustomMenu(menuTitle: string): void { - this.toggleCustomMenu(0); - } - - private toggleCustomMenu(menuIndex: number): void { + private showCustomMenu(menuIndex: number): void { const customMenu = this.customMenus[menuIndex]; - if (this.focusedMenu) { - let hiding: boolean = this.isCurrentMenu(menuIndex); - - // Need to cleanup currently displayed menu - this.cleanupCustomMenu(); - - // Hiding this menu - if (hiding) { - return; - } - } - - customMenu.titleElement.domFocus(); - - let menuHolder = $(customMenu.titleElement).div({ class: 'menubar-menu-items-holder' }); + let menuHolder = $(customMenu.buttonElement).div({ class: 'menubar-menu-items-holder' }); $(menuHolder.getHTMLElement().parentElement).addClass('open'); @@ -665,14 +884,12 @@ export class MenubarPart extends Part { let menuWidget = this._register(new Menu(menuHolder.getHTMLElement(), customMenu.actions, menuOptions)); this._register(menuWidget.onDidCancel(() => { - this.cleanupCustomMenu(); - this.isFocused = false; + this.focusState = MenubarState.FOCUSED; })); this._register(menuWidget.onDidBlur(() => { setTimeout(() => { this.cleanupCustomMenu(); - this.isFocused = false; }, 100); })); @@ -685,25 +902,6 @@ export class MenubarPart extends Part { }; } - updateStyles(): void { - super.updateStyles(); - - // Part container - if (this.container) { - const fgColor = this.getColor(TITLE_BAR_ACTIVE_FOREGROUND); - const bgColor = this.getColor(TITLE_BAR_ACTIVE_BACKGROUND); - - this.container.style('color', fgColor); - if (browser.isFullscreen()) { - this.container.style('background-color', bgColor); - } else { - this.container.style('background-color', null); - } - - toggleClass(this.container.getHTMLElement(), 'light', Color.fromHex(bgColor).isLighter()); - } - } - public get onVisibilityChange(): Event { return this._onVisibilityChange.event; } @@ -728,7 +926,7 @@ export class MenubarPart extends Part { } if (typeof this.initialSizing.menuButtonPaddingLeftRight !== 'number') { - this.initialSizing.menuButtonPaddingLeftRight = parseInt(this.customMenus[0].titleElement.getComputedStyle().paddingLeft, 10); + this.initialSizing.menuButtonPaddingLeftRight = parseInt(this.customMenus[0].buttonElement.getComputedStyle().paddingLeft, 10); } this.container.style({ @@ -739,7 +937,7 @@ export class MenubarPart extends Part { }); this.customMenus.forEach(customMenu => { - customMenu.titleElement.style({ + customMenu.buttonElement.style({ 'padding': `0 ${this.initialSizing.menuButtonPaddingLeftRight / browser.getZoomFactor()}px` }); }); @@ -756,8 +954,8 @@ export class MenubarPart extends Part { public getMenubarItemsDimensions(): Dimension { if (this.customMenus) { - const left = this.customMenus[0].titleElement.getHTMLElement().getBoundingClientRect().left; - const right = this.customMenus[this.customMenus.length - 1].titleElement.getHTMLElement().getBoundingClientRect().right; + const left = this.customMenus[0].buttonElement.getHTMLElement().getBoundingClientRect().left; + const right = this.customMenus[this.customMenus.length - 1].buttonElement.getHTMLElement().getBoundingClientRect().right; return new Dimension(right - left, this.container.getClientArea().height); } @@ -773,83 +971,213 @@ export class MenubarPart extends Part { // Build the menubar if (this.container) { - this.setupMenubar(); + this.doSetupMenubar(); } return this.container.getHTMLElement(); } } +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); + if (menubarActiveWindowFgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button { + color: ${menubarActiveWindowFgColor}; + } + `); + } + + const menubarInactiveWindowFgColor = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND); + if (menubarInactiveWindowFgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar.inactive > .menubar-menu-button { + color: ${menubarInactiveWindowFgColor}; + } + `); + } + + + const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND); + if (menubarSelectedFgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus, + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + color: ${menubarSelectedFgColor}; + } + `); + } + + const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND); + if (menubarSelectedBgColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus, + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + background-color: ${menubarSelectedBgColor}; + } + `); + } + + const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER); + if (menubarSelectedBorderColor) { + collector.addRule(` + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + outline: dashed 1px; + } + + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus { + outline: solid 1px; + } + + .monaco-workbench > .part.menubar > .menubar-menu-button.open, + .monaco-workbench > .part.menubar > .menubar-menu-button:focus, + .monaco-workbench > .part.menubar > .menubar-menu-button:hover { + outline-offset: -1px; + outline-color: ${menubarSelectedBorderColor}; + } + `); + } + + const menuShadow = theme.getColor('widget.shadow'); + if (menuShadow) { + collector.addRule(` + .monaco-shell .monaco-workbench .monaco-menu-container { + box-shadow: 0 2px 4px ${menuShadow}; + } + `); + } + + const menuBgColor = theme.getColor(MENU_BACKGROUND); + if (menuBgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical, + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item { + background-color: ${menuBgColor}; + } + `); + } + + const menuFgColor = theme.getColor(MENU_FOREGROUND); + if (menuFgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical, + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item { + color: ${menuFgColor}; + } + `); + } + + const selectedMenuItemBgColor = theme.getColor(MENU_SELECTION_BACKGROUND); + if (menuBgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused { + background-color: ${selectedMenuItemBgColor}; + } + `); + } + + const selectedMenuItemFgColor = theme.getColor(MENU_SELECTION_FOREGROUND); + if (selectedMenuItemFgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused { + color: ${selectedMenuItemFgColor}; + } + `); + } + + const selectedMenuItemBorderColor = theme.getColor(MENU_SELECTION_BORDER); + if (selectedMenuItemBorderColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused { + border: 1px solid ${selectedMenuItemBorderColor}; + } + `); + } +}); + +type ModifierKey = 'alt' | 'ctrl' | 'shift'; + interface IModifierKeyStatus { altKey: boolean; shiftKey: boolean; ctrlKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; } + class ModifierKeyEmitter extends Emitter { private _subscriptions: IDisposable[] = []; - private _isPressed: IModifierKeyStatus; + private _keyStatus: IModifierKeyStatus; private static instance: ModifierKeyEmitter; private constructor() { super(); - this._isPressed = { + this._keyStatus = { altKey: false, shiftKey: false, ctrlKey: false }; this._subscriptions.push(domEvent(document.body, 'keydown')(e => { - if (e.altKey || e.shiftKey || e.ctrlKey) { - this.isPressed = { - altKey: e.altKey, - ctrlKey: e.ctrlKey, - shiftKey: e.shiftKey - }; + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = 'alt'; + } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = 'ctrl'; + } else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = 'shift'; + } else { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyPressed) { + this.fire(this._keyStatus); } })); this._subscriptions.push(domEvent(document.body, 'keyup')(e => { - if ((!e.altKey && this.isPressed.altKey) || - (!e.shiftKey && this.isPressed.shiftKey) || - (!e.ctrlKey && this.isPressed.ctrlKey) - ) { + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = 'alt'; + } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = 'ctrl'; + } else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = 'shift'; + } else { + this._keyStatus.lastKeyReleased = undefined; + } - this.isPressed = { - altKey: e.altKey, - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey - }; + if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyReleased) { + this.fire(this._keyStatus); } })); - this._subscriptions.push(domEvent(document.body, 'blur')(e => { - if (this.isPressed.altKey || this.isPressed.shiftKey || this.isPressed.ctrlKey) { - this.isPressed = { - altKey: false, - shiftKey: false, - ctrlKey: false - }; - } + this._subscriptions.push(domEvent(window, 'blur')(e => { + this._keyStatus.lastKeyPressed = undefined; + this._keyStatus.lastKeyReleased = undefined; + this._keyStatus.altKey = false; + this._keyStatus.shiftKey = false; + this._keyStatus.shiftKey = false; + + this.fire(this._keyStatus); })); } - get isPressed(): IModifierKeyStatus { - return this._isPressed; - } - - set isPressed(value: IModifierKeyStatus) { - if (this._isPressed.altKey === value.altKey && - this._isPressed.shiftKey === value.shiftKey && - this._isPressed.ctrlKey === value.ctrlKey) { - return; - } - - this._isPressed = value; - this.fire(this._isPressed); - } - static getInstance() { if (!ModifierKeyEmitter.instance) { ModifierKeyEmitter.instance = new ModifierKeyEmitter(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 2ecb5ec8da0..7e88bde2337 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { clearNode, addClass, removeClass, toggleClass, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import URI from 'vs/base/common/uri'; @@ -26,7 +26,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Severity } from 'vs/platform/notification/common/notification'; -export class NotificationsListDelegate implements IDelegate { +export class NotificationsListDelegate implements IVirtualDelegate { private static readonly ROW_HEIGHT = 42; private static readonly LINE_HEIGHT = 22; @@ -278,6 +278,10 @@ export class NotificationRenderer implements IRenderer { +class ListElementDelegate implements IVirtualDelegate { getHeight(element: ListElement): number { return element.item.detail ? 44 : 22; diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 312dae4c272..9a5f5bd6e00 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -12,7 +12,7 @@ visibility: hidden !important; } -.monaco-workbench > .sidebar > .title > .title-label span { +.monaco-workbench > .sidebar > .title > .title-label h2 { text-transform: uppercase; } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 226ccc2b80d..175276033e2 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -9,7 +9,7 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { TPromise } from 'vs/base/common/winjs.base'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { $ } from 'vs/base/browser/builder'; import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -86,15 +86,13 @@ export class StatusbarPart extends Part implements IStatusbarService { container.appendChild(el); } - return { - dispose: () => { - $(el).destroy(); + return toDisposable(() => { + $(el).destroy(); - if (toDispose) { - toDispose.dispose(); - } + if (toDispose) { + toDispose.dispose(); } - }; + }); } private getEntries(alignment: StatusbarAlignment): HTMLElement[] { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index a8330e6fc88..28af45e5b91 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -13,19 +13,27 @@ align-items: center; justify-content: center; user-select: none; - -webkit-app-region: drag; zoom: 1; /* prevent zooming */ line-height: 22px; height: 22px; display: flex; } +.monaco-workbench > .part.titlebar > .titlebar-drag-region { + top: 0; + left: 0; + display: block; + position: absolute; + width: 100%; + height: 100%; + -webkit-app-region: drag; +} + .monaco-workbench > .part.titlebar > .window-title { flex: 0 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - -webkit-app-region: drag; zoom: 1; /* prevent zooming */ } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index d7112d84a45..6188f621f55 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -46,6 +46,7 @@ export class TitlebarPart extends Part implements ITitleService { private titleContainer: Builder; private title: Builder; + private dragRegion: Builder; private windowControls: Builder; private maxRestoreControl: Builder; private appIcon: Builder; @@ -250,15 +251,20 @@ export class TitlebarPart extends Part implements ITitleService { createContentArea(parent: HTMLElement): HTMLElement { this.titleContainer = $(parent); + // Draggable region that we can manipulate for #52522 + this.dragRegion = $(this.titleContainer).div({ class: 'titlebar-drag-region' }); + // App Icon (Windows/Linux) if (!isMacintosh) { - this.appIcon = $(this.titleContainer).div({ - class: 'window-appicon', - }).on(EventType.DBLCLICK, e => { - EventHelper.stop(e, true); + this.appIcon = $(this.titleContainer).div({ class: 'window-appicon' }); - this.windowService.closeWindow().then(null, errors.onUnexpectedError); - }); + if (isWindows) { + this.appIcon.on(EventType.DBLCLICK, e => { + EventHelper.stop(e, true); + + this.windowService.closeWindow().then(null, errors.onUnexpectedError); + }); + } } // Title @@ -500,6 +506,10 @@ export class TitlebarPart extends Part implements ITitleService { let menubarToggled = this.configurationService.getValue('window.menuBarVisibility') === 'toggle'; if (menubarToggled && this.menubarWidth) { this.title.style('visibility', 'hidden'); + + // Hack to fix issue #52522 with layered webkit-app-region elements appearing under cursor + this.dragRegion.hide(); + this.dragRegion.showDelayed(50); } else { this.title.style('visibility', null); } diff --git a/src/vs/workbench/browser/parts/views/media/panelviewlet.css b/src/vs/workbench/browser/parts/views/media/panelviewlet.css index adddb24399d..7d6c4e36d60 100644 --- a/src/vs/workbench/browser/parts/views/media/panelviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/panelviewlet.css @@ -3,8 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-panel-view .panel > .panel-header > .title { +.monaco-panel-view .panel > .panel-header h3.title { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; + font-size: 11px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + display: flex; } diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 8bfdfdae01c..a647bf39021 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -100,7 +100,7 @@ export abstract class ViewletPanel extends Panel implements IView { protected renderHeader(container: HTMLElement): void { this.headerContainer = container; - this.renderHeaderTitle(container); + this.renderHeaderTitle(container, this.title); const actions = append(container, $('.actions')); this.toolbar = new ToolBar(actions, this.contextMenuService, { @@ -119,8 +119,8 @@ export abstract class ViewletPanel extends Panel implements IView { this.updateActionsVisibility(); } - protected renderHeaderTitle(container: HTMLElement): void { - append(container, $('.title', null, this.title)); + protected renderHeaderTitle(container: HTMLElement, title: string): void { + append(container, $('h3.title', null, title)); } focus(): void { diff --git a/src/vs/workbench/buildfile.js b/src/vs/workbench/buildfile.js index 4b83a39ab10..1dfb66c06b4 100644 --- a/src/vs/workbench/buildfile.js +++ b/src/vs/workbench/buildfile.js @@ -27,8 +27,6 @@ exports.collectModules = function () { createModuleDescription('vs/workbench/services/files/node/watcher/nsfw/watcherApp', []), createModuleDescription('vs/workbench/node/extensionHostProcess', []), - - createModuleDescription('vs/workbench/parts/terminal/node/terminalProcess', []) ]; return modules; diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 94120adb639..8dfe3cd10c7 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, listActiveSelectionForeground, listActiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; import { Disposable } from 'vs/base/common/lifecycle'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; @@ -388,6 +388,56 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { hc: contrastBorder }, nls.localize('titleBarBorder', "Title bar border color. Note that this color is currently only supported on macOS.")); +// < --- Menubar --- > + +export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { + dark: TITLE_BAR_ACTIVE_FOREGROUND, + light: TITLE_BAR_ACTIVE_FOREGROUND, + hc: TITLE_BAR_ACTIVE_FOREGROUND +}, nls.localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); + +export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { + dark: transparent(Color.white, 0.1), + light: transparent(Color.black, 0.1), + hc: null +}, nls.localize('menubarSelectionBackground', "Background color of the selected menu item in the menubar.")); + +export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', { + dark: null, + light: null, + hc: activeContrastBorder +}, nls.localize('menubarSelectionBorder', "Border color of the selected menu item in the menubar.")); + +export const MENU_FOREGROUND = registerColor('menu.foreground', { + dark: SIDE_BAR_FOREGROUND, + light: SIDE_BAR_FOREGROUND, + hc: SIDE_BAR_FOREGROUND +}, nls.localize('menuForeground', "Foreground color of menu items.")); + +export const MENU_BACKGROUND = registerColor('menu.background', { + dark: SIDE_BAR_BACKGROUND, + light: SIDE_BAR_BACKGROUND, + hc: SIDE_BAR_BACKGROUND +}, nls.localize('menuBackground', "Background color of menu items.")); + +export const MENU_SELECTION_FOREGROUND = registerColor('menu.selectionForeground', { + dark: listActiveSelectionForeground, + light: listActiveSelectionForeground, + hc: listActiveSelectionForeground +}, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); + +export const MENU_SELECTION_BACKGROUND = registerColor('menu.selectionBackground', { + dark: listActiveSelectionBackground, + light: listActiveSelectionBackground, + hc: listActiveSelectionBackground +}, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); + +export const MENU_SELECTION_BORDER = registerColor('menu.selectionBorder', { + dark: null, + light: null, + hc: null +}, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); + // < --- Notifications --- > export const NOTIFICATIONS_CENTER_BORDER = registerColor('notificationCenter.border', { diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index c6fb39e0bc0..2a4764e9943 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -36,7 +36,7 @@ import { webFrame, shell } from 'electron'; import { getPathLabel, getBaseLabel } from 'vs/base/common/labels'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IPanel } from 'vs/workbench/common/panel'; -import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService, ActivationTimes } from 'vs/workbench/services/extensions/common/extensions'; @@ -598,8 +598,8 @@ export abstract class BaseSwitchWindow extends Action { const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); const picks = windows.map(win => ({ payload: win.id, - resource: win.filename ? URI.file(win.filename) : win.folderPath ? URI.file(win.folderPath) : win.workspace ? URI.file(win.workspace.configPath) : void 0, - fileKind: win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderPath ? FileKind.FOLDER : FileKind.FILE, + resource: win.filename ? URI.file(win.filename) : win.folderUri ? win.folderUri : win.workspace ? URI.file(win.workspace.configPath) : void 0, + fileKind: win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderUri ? FileKind.FOLDER : FileKind.FILE, label: win.title, description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : void 0, run: () => { @@ -723,22 +723,26 @@ export abstract class BaseOpenRecentAction extends Action { private openRecent(recentWorkspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], recentFiles: string[]): void { - function toPick(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, separator: ISeparator, fileKind: FileKind, environmentService: IEnvironmentService, removeAction?: RemoveFromRecentlyOpened): IFilePickOpenEntry { - let path: string; + function toPick(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, separator: ISeparator, fileKind: FileKind, environmentService: IEnvironmentService, removeAction?: RemoveFromRecentlyOpened): IFilePickOpenEntry { + let resource: URI; let label: string; let description: string; if (isSingleFolderWorkspaceIdentifier(workspace)) { - path = workspace; - label = getBaseLabel(path); - description = getPathLabel(paths.dirname(path), environmentService); - } else { - path = workspace.configPath; + resource = workspace; + label = getWorkspaceLabel(workspace, environmentService); + description = getPathLabel(resource.with({ path: paths.dirname(resource.path) }), environmentService); + } else if (isWorkspaceIdentifier(workspace)) { + resource = URI.file(workspace.configPath); label = getWorkspaceLabel(workspace, environmentService); description = getPathLabel(paths.dirname(workspace.configPath), environmentService); + } else { + resource = URI.file(workspace); + label = getBaseLabel(workspace); + description = getPathLabel(paths.dirname(workspace), environmentService); } return { - resource: URI.file(path), + resource, fileKind, label, description, @@ -747,16 +751,16 @@ export abstract class BaseOpenRecentAction extends Action { setTimeout(() => { // Bug: somehow when not running this code in a timeout, it is not possible to use this picker // with quick navigate keys (not able to trigger quick navigate once running it once). - runPick(path, fileKind === FileKind.FILE, context); + runPick(resource, fileKind === FileKind.FILE, context); }); }, action: removeAction }; } - const runPick = (path: string, isFile: boolean, context: IEntryRunContext) => { + const runPick = (resource: URI, isFile: boolean, context: IEntryRunContext) => { const forceNewWindow = context.keymods.ctrlCmd; - this.windowService.openWindow([path], { forceNewWindow, forceOpenWorkspaceAsFile: isFile }); + this.windowService.openWindow([resource], { forceNewWindow, forceOpenWorkspaceAsFile: isFile }); }; const workspacePicks: IFilePickOpenEntry[] = recentWorkspaces.map((workspace, index) => toPick(workspace, index === 0 ? { label: nls.localize('workspaces', "workspaces") } : void 0, isSingleFolderWorkspaceIdentifier(workspace) ? FileKind.FOLDER : FileKind.ROOT_FOLDER, this.environmentService, !this.isQuickNavigate() ? this.removeAction : void 0)); diff --git a/src/vs/workbench/electron-browser/commands.ts b/src/vs/workbench/electron-browser/commands.ts index ecb6e315f43..0c1ea832e8e 100644 --- a/src/vs/workbench/electron-browser/commands.ts +++ b/src/vs/workbench/electron-browser/commands.ts @@ -13,12 +13,13 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind import { List } from 'vs/base/browser/ui/list/listWidget'; import * as errors from 'vs/base/common/errors'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget } from 'vs/platform/list/browser/listService'; +import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus } from 'vs/platform/list/browser/listService'; import { PagedList } from 'vs/base/browser/ui/list/listPaging'; import { range } from 'vs/base/common/arrays'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import { InEditorZenModeContext, NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; +import { ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; // --- List Commands @@ -32,6 +33,7 @@ function ensureDOMFocus(widget: ListWidget): void { } } +export const QUIT_ID = 'workbench.action.quit'; export function registerCommands(): void { function focusDown(accessor: ServicesAccessor, arg2?: number): void { @@ -105,7 +107,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.expandSelectionDown', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), primary: KeyMod.Shift | KeyCode.DownArrow, handler: (accessor, arg2) => { const focused = accessor.get(IListService).lastFocusedList; @@ -178,7 +180,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.expandSelectionUp', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), primary: KeyMod.Shift | KeyCode.UpArrow, handler: (accessor, arg2) => { const focused = accessor.get(IListService).lastFocusedList; @@ -471,13 +473,30 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.clear', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListHasSelectionOrFocus), primary: KeyCode.Escape, handler: (accessor) => { const focused = accessor.get(IListService).lastFocusedList; - // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + if (list.getSelection().length > 0) { + list.setSelection([]); + + return void 0; + } + + if (list.getFocus().length > 0) { + list.setFocus([]); + + return void 0; + } + } + + // Tree + else if (focused) { const tree = focused; if (tree.getSelection().length) { @@ -520,7 +539,7 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.quit', + id: QUIT_ID, weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), handler(accessor: ServicesAccessor) { const windowsService = accessor.get(IWindowsService); @@ -531,7 +550,7 @@ export function registerCommands(): void { win: { primary: void 0 } }); - CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, path: string) { + CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, path: string | ISingleFolderWorkspaceIdentifier) { const windowsService = accessor.get(IWindowsService); return windowsService.removeFromRecentlyOpened([path]).then(() => void 0); diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 6518e436b43..847b5e6972f 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -14,8 +14,8 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseWorkspaceAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, ShowAboutDialogAction, InspectContextKeysAction, OpenProcessExplorer, OpenTwitterUrlAction, OpenRequestFeatureUrlAction, OpenPrivacyStatementUrlAction, OpenLicenseUrlAction, ShowAccessibilityOptionsAction } from 'vs/workbench/electron-browser/actions'; -import { registerCommands } from 'vs/workbench/electron-browser/commands'; +import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseWorkspaceAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, ShowAboutDialogAction, InspectContextKeysAction, OpenProcessExplorer, OpenTwitterUrlAction, OpenRequestFeatureUrlAction, OpenPrivacyStatementUrlAction, OpenLicenseUrlAction, ShowAccessibilityOptionsAction, OpenRecentAction } from 'vs/workbench/electron-browser/actions'; +import { registerCommands, QUIT_ID } from 'vs/workbench/electron-browser/commands'; import { AddRootFolderAction, GlobalRemoveRootFolderAction, OpenWorkspaceAction, SaveWorkspaceAsAction, OpenWorkspaceConfigFileAction, DuplicateWorkspaceInNewWindowAction, OpenFileFolderAction, OpenFileAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; @@ -152,6 +152,172 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R } }); +// Menu registration - file menu + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '1_new', + command: { + id: NewWindowAction.ID, + title: nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenFileAction.ID, + title: nls.localize({ key: 'miOpenFile', comment: ['&& denotes a mnemonic'] }, "&&Open File...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenFolderAction.ID, + title: nls.localize({ key: 'miOpenFolder', comment: ['&& denotes a mnemonic'] }, "Open &&Folder...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenWorkspaceAction.ID, + title: nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"), + submenu: MenuId.MenubarRecentMenu, + group: '2_open', + order: 4 +}); + + +// More +MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: 'y_more', + command: { + id: OpenRecentAction.ID, + title: nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '3_workspace', + command: { + id: AddRootFolderAction.ID, + title: nls.localize({ key: 'miAddFolderToWorkspace', comment: ['&& denotes a mnemonic'] }, "A&&dd Folder to Workspace...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '3_workspace', + command: { + id: SaveWorkspaceAsAction.ID, + title: nls.localize('miSaveWorkspaceAs', "Save Workspace As...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences"), + submenu: MenuId.MenubarPreferencesMenu, + group: '5_autosave', + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CloseWorkspaceAction.ID, + title: nls.localize({ key: 'miCloseFolder', comment: ['&& denotes a mnemonic'] }, "Close &&Folder") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CloseCurrentWindowAction.ID, + title: nls.localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window") + }, + order: 4 +}); + +if (!isMacintosh) { + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: 'z_Exit', + command: { + id: QUIT_ID, + title: nls.localize({ key: 'miExit', comment: ['&& denotes a mnemonic'] }, "E&&xit") + }, + order: 1 + }); +} + +// Appereance menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_appearance', + title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"), + submenu: MenuId.MenubarAppearanceMenu, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleFullScreenAction.ID, + title: nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleMenuBarAction.ID, + title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") + }, + order: 4 +}); + +// Zoom + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', + command: { + id: ZoomInAction.ID, + title: nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', + command: { + id: ZoomOutAction.ID, + title: nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', + command: { + id: ZoomResetAction.ID, + title: nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") + }, + order: 3 +}); + + // Configuration: Workbench const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -463,7 +629,7 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('window.smoothScrollingWorkaround', "Enable this workaround if scrolling is no longer smooth after restoring a minimized VS Code window. This is a workaround for an issue (https://github.com/Microsoft/vscode/issues/13612) where scrolling starts to lag on devices with precision trackpads like the Surface devices from Microsoft. Enabling this workaround can result in a little bit of layout flickering after restoring the window from minimized state but is otherwise harmless."), + 'description': nls.localize('window.smoothScrollingWorkaround', "Enable this workaround if scrolling is no longer smooth after restoring a minimized VS Code window. This is a workaround for an issue (https://github.com/Microsoft/vscode/issues/13612) where scrolling starts to lag on devices with precision trackpads like the Surface devices from Microsoft. Enabling this workaround can result in a little bit of layout flickering after restoring the window from minimized state but is otherwise harmless. Note: in order for this workaround to function, make sure to also set 'window.titleBarStyle: native'."), 'included': isWindows }, 'window.clickThroughInactive': { diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index a51785aea26..96bcb26b2a0 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -39,7 +39,7 @@ import { IUpdateService } from 'vs/platform/update/common/update'; import { URLHandlerChannel, URLServiceChannelClient } from 'vs/platform/url/common/urlIpc'; import { IURLService } from 'vs/platform/url/common/url'; import { WorkspacesChannelClient } from 'vs/platform/workspaces/common/workspacesIpc'; -import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesService, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; import * as fs from 'fs'; import { ConsoleLogService, MultiplexLogService, ILogService } from 'vs/platform/log/common/log'; @@ -49,6 +49,7 @@ import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log import { RelayURLService } from 'vs/platform/url/common/urlService'; import { MenubarChannelClient } from 'vs/platform/menubar/common/menubarIpc'; import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { Schemas } from 'vs/base/common/network'; gracefulFs.gracefulify(fs); // enable gracefulFs @@ -115,22 +116,24 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { } function createAndInitializeWorkspaceService(configuration: IWindowConfiguration, environmentService: EnvironmentService): TPromise { - return validateSingleFolderPath(configuration).then(() => { + const folderUri = configuration.folderUri ? uri.revive(configuration.folderUri) : null; + return validateFolderUri(folderUri, configuration.verbose).then(validatedFolderUri => { + const workspaceService = new WorkspaceService(environmentService); - return workspaceService.initialize(configuration.workspace || configuration.folderPath || configuration).then(() => workspaceService, error => workspaceService); + return workspaceService.initialize(configuration.workspace || validatedFolderUri || configuration).then(() => workspaceService, error => workspaceService); }); } -function validateSingleFolderPath(configuration: IWindowConfiguration): TPromise { +function validateFolderUri(folderUri: ISingleFolderWorkspaceIdentifier, verbose: boolean): TPromise { - // Return early if we do not have a single folder path - if (!configuration.folderPath) { - return TPromise.as(void 0); + // Return early if we do not have a single folder uri or if it is a non file uri + if (!folderUri || folderUri.scheme !== Schemas.file) { + return TPromise.as(folderUri); } // Otherwise: use realpath to resolve symbolic links to the truth - return realpath(configuration.folderPath).then(realFolderPath => { + return realpath(folderUri.fsPath).then(realFolderPath => { // For some weird reason, node adds a trailing slash to UNC paths // we never ever want trailing slashes as our workspace path unless @@ -140,19 +143,19 @@ function validateSingleFolderPath(configuration: IWindowConfiguration): TPromise realFolderPath = strings.rtrim(realFolderPath, paths.nativeSep); } - return realFolderPath; + return uri.file(realFolderPath); }, error => { - if (configuration.verbose) { + if (verbose) { errors.onUnexpectedError(error); } // Treat any error case as empty workbench case (no folder path) return null; - }).then(realFolderPathOrNull => { + }).then(realFolderUriOrNull => { // Update config with real path if we have one - configuration.folderPath = realFolderPathOrNull; + return realFolderUriOrNull; }); } diff --git a/src/vs/workbench/electron-browser/media/shell.css b/src/vs/workbench/electron-browser/media/shell.css index 64869f0de97..4911010c77c 100644 --- a/src/vs/workbench/electron-browser/media/shell.css +++ b/src/vs/workbench/electron-browser/media/shell.css @@ -69,9 +69,13 @@ padding: .5em 0; } +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-menu-item { + height: 1.8em; +} + .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; + padding: 0 1.5em; } .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label.separator { @@ -80,7 +84,7 @@ } .monaco-shell .monaco-menu .monaco-action-bar.vertical .submenu-indicator { - padding: 0.5em 1em; + padding: 0 1em; } .monaco-shell .monaco-menu .action-item { @@ -107,7 +111,6 @@ .monaco-shell input[type="button"]:active, .monaco-shell input[type="checkbox"]:active, .monaco-shell .monaco-tree .monaco-tree-row -.monaco-action-bar .action-item [tabindex="0"]:hover, .monaco-shell .monaco-tree.focused.no-focused-item:active:before { outline: 0 !important; /* fixes some flashing outlines from showing up when clicking */ } diff --git a/src/vs/workbench/electron-browser/media/workbench.css b/src/vs/workbench/electron-browser/media/workbench.css index 0968fc3040e..28c4a57c000 100644 --- a/src/vs/workbench/electron-browser/media/workbench.css +++ b/src/vs/workbench/electron-browser/media/workbench.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench-container { - position: absolute; -} - .monaco-workbench { font-size: 13px; line-height: 1.4em; diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 1096104def6..73dde75f2bb 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -16,10 +16,9 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import product from 'vs/platform/node/product'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import pkg from 'vs/platform/node/package'; -import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { Workbench, IWorkbenchStartedInfo } from 'vs/workbench/electron-browser/workbench'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService, configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryService, configurationTelemetry, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; @@ -46,7 +45,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ILifecycleService, LifecyclePhase, ShutdownReason, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -93,7 +91,7 @@ import { NotificationService } from 'vs/workbench/services/notification/common/n import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; import { DialogChannel } from 'vs/platform/dialogs/common/dialogIpc'; -import { EventType, addDisposableListener, addClass, getClientArea } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, addClass } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { SearchHistoryService } from 'vs/workbench/services/search/node/searchHistoryService'; @@ -120,7 +118,6 @@ export class WorkbenchShell extends Disposable { private storageService: IStorageService; private environmentService: IEnvironmentService; private logService: ILogService; - private contextViewService: ContextViewService; private configurationService: IConfigurationService; private contextService: IWorkspaceContextService; private telemetryService: ITelemetryService; @@ -135,8 +132,6 @@ export class WorkbenchShell extends Disposable { private container: HTMLElement; private previousErrorValue: string; private previousErrorTime: number; - private content: HTMLElement; - private contentsContainer: HTMLElement; private configuration: IWindowConfiguration; private workbench: Workbench; @@ -160,20 +155,15 @@ export class WorkbenchShell extends Disposable { this.previousErrorTime = 0; } - private createContents(parent: HTMLElement): HTMLElement { - + private renderContents(): void { // ARIA aria.setARIAContainer(document.body); - // Workbench Container - const workbenchContainer = document.createElement('div'); - parent.appendChild(workbenchContainer); - // Instantiation service with services - const [instantiationService, serviceCollection] = this.initServiceCollection(parent); + const [instantiationService, serviceCollection] = this.initServiceCollection(this.container); // Workbench - this.workbench = this.createWorkbench(instantiationService, serviceCollection, parent, workbenchContainer); + this.workbench = this.createWorkbench(instantiationService, serviceCollection, this.container); // Window this.workbench.getInstantiationService().createInstance(ElectronWindow); @@ -186,13 +176,11 @@ export class WorkbenchShell extends Disposable { this.lifecycleService.when(LifecyclePhase.Running).then(() => { clearTimeout(timeoutHandle); }); - - return workbenchContainer; } - private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, parent: HTMLElement, workbenchContainer: HTMLElement): Workbench { + private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, container: HTMLElement): Workbench { try { - const workbench = instantiationService.createInstance(Workbench, parent, workbenchContainer, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); + const workbench = instantiationService.createInstance(Workbench, container, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); // Set lifecycle phase to `Restoring` this.lifecycleService.phase = LifecyclePhase.Restoring; @@ -362,14 +350,12 @@ export class WorkbenchShell extends Disposable { serviceCollection.set(IHashService, new SyncDescriptor(HashService)); // Telemetry - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { - const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); - const commit = product.commit; - const version = pkg.version; + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); const config: ITelemetryServiceConfig = { - appender: new TelemetryAppenderClient(channel), - commonProperties: resolveWorkbenchCommonProperties(this.storageService, commit, version, this.configuration.machineId, this.environmentService.installSourcePath), + appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)), + commonProperties: resolveWorkbenchCommonProperties(this.storageService, product.commit, pkg.version, this.configuration.machineId, this.environmentService.installSourcePath), piiPaths: [this.environmentService.appRoot, this.environmentService.extensionsPath] }; @@ -418,9 +404,6 @@ export class WorkbenchShell extends Disposable { serviceCollection.set(ICommandService, new SyncDescriptor(CommandService)); - this.contextViewService = instantiationService.createInstance(ContextViewService, this.container); - serviceCollection.set(IContextViewService, this.contextViewService); - serviceCollection.set(IMarkerService, new SyncDescriptor(MarkerService)); serviceCollection.set(IModeService, new SyncDescriptor(WorkbenchModeServiceImpl)); @@ -472,13 +455,8 @@ export class WorkbenchShell extends Disposable { // Shell Class for CSS Scoping addClass(this.container, 'monaco-shell'); - // Controls - this.content = document.createElement('div'); - addClass(this.content, 'monaco-shell-content'); - this.container.appendChild(this.content); - // Create Contents - this.contentsContainer = this.createContents(this.content); + this.renderContents(); // Layout this.layout(); @@ -521,12 +499,6 @@ export class WorkbenchShell extends Disposable { } private layout(): void { - const clientArea = getClientArea(this.container); - - this.contentsContainer.style.width = `${clientArea.width}px`; - this.contentsContainer.style.height = `${clientArea.height}px`; - - this.contextViewService.layout(); this.workbench.layout(); } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index a87a8f73f31..d01be333d42 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -66,7 +66,7 @@ export class ElectronWindow extends Themable { private previousConfiguredZoomLevel: number; private addFoldersScheduler: RunOnceScheduler; - private pendingFoldersToAdd: IAddFoldersRequest[]; + private pendingFoldersToAdd: URI[]; constructor( @IEditorService private editorService: EditorServiceImpl, @@ -404,7 +404,7 @@ export class ElectronWindow extends Themable { private onAddFoldersRequest(request: IAddFoldersRequest): void { // Buffer all pending requests - this.pendingFoldersToAdd.push(request); + this.pendingFoldersToAdd.push(...request.foldersToAdd.map(f => URI.revive(f))); // Delay the adding of folders a bit to buffer in case more requests are coming if (!this.addFoldersScheduler.isScheduled()) { @@ -415,8 +415,8 @@ export class ElectronWindow extends Themable { private doAddFolders(): void { const foldersToAdd: IWorkspaceFolderCreationData[] = []; - this.pendingFoldersToAdd.forEach(request => { - foldersToAdd.push(...request.foldersToAdd.map(folderToAdd => ({ uri: URI.file(folderToAdd.filePath) }))); + this.pendingFoldersToAdd.forEach(folder => { + foldersToAdd.push(({ uri: folder })); }); this.pendingFoldersToAdd = []; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 7191ec03a8c..00aae6933f9 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -191,7 +191,6 @@ export class Workbench extends Disposable implements IPartService { _serviceBrand: any; private workbenchParams: WorkbenchParams; - private workbenchContainer: Builder; private workbench: Builder; private workbenchStarted: boolean; private workbenchCreated: boolean; @@ -200,6 +199,7 @@ export class Workbench extends Disposable implements IPartService { private editorService: EditorService; private editorGroupService: IEditorGroupsService; private viewletService: IViewletService; + private contextViewService: ContextViewService; private contextKeyService: IContextKeyService; private keybindingService: IKeybindingService; private backupFileService: IBackupFileService; @@ -236,7 +236,6 @@ export class Workbench extends Disposable implements IPartService { private closeEmptyWindowScheduler: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50)); constructor( - private parent: HTMLElement, private container: HTMLElement, private configuration: IWindowConfiguration, serviceCollection: ServiceCollection, @@ -250,7 +249,6 @@ export class Workbench extends Disposable implements IPartService { @IEnvironmentService private environmentService: IEnvironmentService, @IWindowService private windowService: IWindowService, @INotificationService private notificationService: NotificationService, - @IContextViewService private contextViewService: ContextViewService, @ITelemetryService private telemetryService: TelemetryService ) { super(); @@ -300,11 +298,10 @@ export class Workbench extends Disposable implements IPartService { } private createWorkbench(): void { - this.workbenchContainer = $('.monaco-workbench-container'); this.workbench = $().div({ 'class': `monaco-workbench ${isWindows ? 'windows' : isLinux ? 'linux' : 'mac'}`, id: Identifiers.WORKBENCH_CONTAINER - }).appendTo(this.workbenchContainer); + }); } private createGlobalActions(): void { @@ -355,6 +352,10 @@ export class Workbench extends Disposable implements IPartService { // List serviceCollection.set(IListService, this.instantiationService.createInstance(ListService)); + // Context view service + this.contextViewService = this.instantiationService.createInstance(ContextViewService, this.workbench.getHTMLElement()); + serviceCollection.set(IContextViewService, this.contextViewService); + // Use themable context menus when custom titlebar is enabled to match custom menubar if (!isMacintosh && this.getCustomTitleBarStyle() === 'custom') { serviceCollection.set(IContextMenuService, new SyncDescriptor(HTMLContextMenuService, null, this.telemetryService, this.notificationService, this.contextViewService)); @@ -707,7 +708,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 @@ -970,12 +976,6 @@ export class Workbench extends Disposable implements IPartService { // Apply font aliasing this.setFontAliasing(this.fontAliasing); - // Apply title style if shown - const titleStyle = this.getCustomTitleBarStyle(); - if (titleStyle) { - DOM.addClass(this.parent, `titlebar-style-${titleStyle}`); - } - // Apply fullscreen state if (browser.isFullscreen()) { this.workbench.addClass('fullscreen'); @@ -994,7 +994,7 @@ export class Workbench extends Disposable implements IPartService { this.createNotificationsHandlers(); // Add Workbench to DOM - this.workbenchContainer.build(this.container); + this.workbench.appendTo(this.container); } private createTitlebarPart(): void { @@ -1287,6 +1287,8 @@ export class Workbench extends Disposable implements IPartService { } layout(options?: ILayoutOptions): void { + this.contextViewService.layout(); + if (this.workbenchStarted && !this.workbenchShutdown) { this.workbenchLayout.layout(options); } 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..54006b5ff83 100644 --- a/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts +++ b/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts @@ -70,20 +70,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 +97,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 +128,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 +146,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/codeEditor/electron-browser/toggleMinimap.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts index 8361e7b855b..36617f7b8c3 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export class ToggleMinimapAction extends Action { public static readonly ID = 'editor.action.toggleMinimap'; @@ -32,3 +32,12 @@ export class ToggleMinimapAction extends Action { const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMinimapAction, ToggleMinimapAction.ID, ToggleMinimapAction.LABEL), 'View: Toggle Minimap'); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: ToggleMinimapAction.ID, + title: nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap") + }, + order: 2 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts index 7c35e71b00c..42c4e6bf068 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export class ToggleRenderControlCharacterAction extends Action { @@ -33,3 +33,12 @@ export class ToggleRenderControlCharacterAction extends Action { const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRenderControlCharacterAction, ToggleRenderControlCharacterAction.ID, ToggleRenderControlCharacterAction.LABEL), 'View: Toggle Control Characters'); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: ToggleRenderControlCharacterAction.ID, + title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters") + }, + order: 4 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts index 856e35dce60..d9a61a863eb 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export class ToggleRenderWhitespaceAction extends Action { @@ -41,3 +41,12 @@ export class ToggleRenderWhitespaceAction extends Action { const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRenderWhitespaceAction, ToggleRenderWhitespaceAction.ID, ToggleRenderWhitespaceAction.LABEL), 'View: Toggle Render Whitespace'); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: ToggleRenderWhitespaceAction.ID, + title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace") + }, + order: 3 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts index bd18b1271c4..882cd01e68b 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts @@ -130,11 +130,12 @@ function applyWordWrapState(editor: ICodeEditor, state: IWordWrapState): void { }); } +const TOGGLE_WORD_WRAP_ID = 'editor.action.toggleWordWrap'; class ToggleWordWrapAction extends EditorAction { constructor() { super({ - id: 'editor.action.toggleWordWrap', + id: TOGGLE_WORD_WRAP_ID, label: nls.localize('toggle.wordwrap', "View: Toggle Word Wrap"), alias: 'View: Toggle Word Wrap', precondition: null, @@ -255,7 +256,7 @@ registerEditorAction(ToggleWordWrapAction); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { - id: 'editor.action.toggleWordWrap', + id: TOGGLE_WORD_WRAP_ID, title: nls.localize('unwrapMinified', "Disable wrapping for this file"), iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/media/WordWrap_16x.svg')) } }, @@ -269,7 +270,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { - id: 'editor.action.toggleWordWrap', + id: TOGGLE_WORD_WRAP_ID, title: nls.localize('wrapMinified', "Enable wrapping for this file"), iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/media/WordWrap_16x.svg')) } }, @@ -281,3 +282,14 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { ContextKeyExpr.not(isWordWrapMinifiedKey) ) }); + + +// View menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: TOGGLE_WORD_WRAP_ID, + title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index e06e3c0ea21..41fd94bf268 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -20,19 +20,20 @@ import { peekViewBorder } from 'vs/editor/contrib/referenceSearch/referencesWidg import { IOptions, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { CommentGlyphWidget } from 'vs/workbench/parts/comments/electron-browser/commentGlyphWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SimpleCommentEditor } from './simpleCommentEditor'; import URI from 'vs/base/common/uri'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { transparent, editorForeground, inputValidationErrorBorder, textLinkActiveForeground, textLinkForeground, focusBorder, textBlockQuoteBackground, textBlockQuoteBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const EXPAND_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-down'; @@ -47,7 +48,10 @@ export class CommentNode { public get domNode(): HTMLElement { return this._domNode; } - constructor(public comment: modes.Comment) { + constructor( + public comment: modes.Comment, + private markdownRenderer: MarkdownRenderer, + ) { this._domNode = $('div.review-comment').getHTMLElement(); this._domNode.tabIndex = 0; let avatar = $('div.avatar-container').appendTo(this._domNode).getHTMLElement(); @@ -59,7 +63,7 @@ export class CommentNode { let author = $('strong.author').appendTo(header).getHTMLElement(); author.innerText = comment.userName; this._body = $('div.comment-body').appendTo(commentDetailsContainer).getHTMLElement(); - this._md = renderMarkdown(comment.body); + this._md = this.markdownRenderer.render(comment.body).element; this._body.appendChild(this._md); this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); @@ -70,7 +74,7 @@ export class CommentNode { update(newComment: modes.Comment) { if (newComment.body !== this.comment.body) { this._body.removeChild(this._md); - this._md = renderMarkdown(newComment.body); + this._md = this.markdownRenderer.render(newComment.body).element; this._body.appendChild(this._md); } @@ -108,8 +112,9 @@ export class ReviewZoneWidget extends ZoneWidget { private _commentThread: modes.CommentThread; private _commentGlyph: CommentGlyphWidget; private _owner: number; - private _decorationIDs: string[]; private _localToDispose: IDisposable[]; + private _markdownRenderer: MarkdownRenderer; + private _styleElement: HTMLStyleElement; public get owner(): number { return this._owner; @@ -124,6 +129,7 @@ export class ReviewZoneWidget extends ZoneWidget { private modelService: IModelService, private themeService: IThemeService, private commentService: ICommentService, + private openerService: IOpenerService, editor: ICodeEditor, owner: number, commentThread: modes.CommentThread, @@ -134,10 +140,14 @@ 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._styleElement = dom.createStyleSheet(this.domNode); this.themeService.onThemeChange(this._applyTheme, this); + this._applyTheme(this.themeService.getTheme()); + + this._markdownRenderer = new MarkdownRenderer(editor, this.modeService, this.openerService); } public get onDidClose(): Event { @@ -150,12 +160,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(); @@ -195,21 +200,18 @@ 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(), {}); this._disposables.push(this._actionbarWidget); - this._toggleAction = new Action('review.expand', nls.localize('label.expand', "Expand"), this._isCollapsed ? EXPAND_ACTION_CLASS : COLLAPSE_ACTION_CLASS, true, () => { + this._toggleAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), this._isCollapsed ? EXPAND_ACTION_CLASS : COLLAPSE_ACTION_CLASS, true, () => { if (this._isCollapsed) { this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); + this._toggleAction.label = nls.localize('label.collapse', "Collapse"); } else { if (this._commentThread.comments.length === 0) { @@ -218,6 +220,7 @@ export class ReviewZoneWidget extends ZoneWidget { } this._isCollapsed = true; this.hide(); + this._toggleAction.label = nls.localize('label.expand', "Expand"); } return null; }); @@ -253,16 +256,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--) { @@ -272,7 +265,7 @@ export class ReviewZoneWidget extends ZoneWidget { lastCommentElement = oldCommentNode[0].domNode; newCommentNodeList.unshift(oldCommentNode[0]); } else { - let newElement = new CommentNode(currentComment); + let newElement = new CommentNode(currentComment, this._markdownRenderer); newCommentNodeList.unshift(newElement); if (lastCommentElement) { this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); @@ -291,7 +284,7 @@ export class ReviewZoneWidget extends ZoneWidget { } protected _doLayout(heightInPixel: number, widthInPixel: number): void { - this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 20 /* margin */ }); + this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 40 /* margin */ }); } display(lineNumber: number) { @@ -331,7 +324,7 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentElements = []; for (let i = 0; i < this._commentThread.comments.length; i++) { - let newCommentNode = new CommentNode(this._commentThread.comments[i]); + let newCommentNode = new CommentNode(this._commentThread.comments[i], this._markdownRenderer); this._commentElements.push(newCommentNode); this._commentsElement.appendChild(newCommentNode.domNode); } @@ -349,7 +342,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.createReplyButton(); + 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; @@ -366,6 +367,22 @@ export class ReviewZoneWidget extends ZoneWidget { attachButtonStyler(button, this.themeService); button.label = 'Add comment'; button.onDidClick(async () => { + if (!this._commentEditor.getValue()) { + this._commentEditor.focus(); + this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; + + + this._disposables.push(this._commentEditor.onDidChangeModelContent(_ => { + if (!this._commentEditor.getValue()) { + this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; + } else { + this._commentEditor.getDomNode().style.outline = ''; + } + })); + + return; + } + let newCommentThread; if (this._commentThread.threadId) { // reply @@ -385,6 +402,7 @@ export class ReviewZoneWidget extends ZoneWidget { ); this.createReplyButton(); + this.createParticipantsLabel(); } this._commentEditor.setValue(''); @@ -416,31 +434,33 @@ export class ReviewZoneWidget extends ZoneWidget { } } - createReplyButton() { - 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 { + 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() { @@ -457,26 +477,28 @@ export class ReviewZoneWidget extends ZoneWidget { } private setCommentEditorDecorations() { - let model = this._commentEditor.getModel(); - let valueLength = model.getValueLength(); - const hasExistingComments = this._commentThread.comments.length > 0; - let placeholder = valueLength > 0 ? '' : (hasExistingComments ? 'Reply...' : 'Type a new comment'); - const decorations = [{ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: placeholder, - color: transparent(editorForeground, 0.4)(this.themeService.getTheme()).toString() + const model = this._commentEditor && this._commentEditor.getModel(); + if (model) { + let valueLength = model.getValueLength(); + const hasExistingComments = this._commentThread.comments.length > 0; + let placeholder = valueLength > 0 ? '' : (hasExistingComments ? 'Reply...' : 'Type a new comment'); + const decorations = [{ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: placeholder, + color: transparent(editorForeground, 0.4)(this.themeService.getTheme()).toString() + } } - } - }]; + }]; - this._commentEditor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations); + this._commentEditor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations); + } } private mouseDownInfo: { lineNumber: number, iconClicked: boolean }; @@ -542,6 +564,44 @@ export class ReviewZoneWidget extends ZoneWidget { arrowColor: borderColor, frameColor: borderColor }); + + const content: string[] = []; + const linkColor = theme.getColor(textLinkForeground); + if (linkColor) { + content.push(`.monaco-editor .review-widget .body .review-comment a { color: ${linkColor} }`); + } + + const linkActiveColor = theme.getColor(textLinkActiveForeground); + if (linkActiveColor) { + content.push(`.monaco-editor .review-widget .body .review-comment a:hover, a:active { color: ${linkActiveColor} }`); + } + + const focusColor = theme.getColor(focusBorder); + if (focusColor) { + content.push(`.monaco-editor .review-widget .body .review-comment a:focus { outline: 1px solid ${focusColor}; }`); + content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); + } + + const blockQuoteBackground = theme.getColor(textBlockQuoteBackground); + if (blockQuoteBackground) { + content.push(`.monaco-editor .review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`); + } + + const blockQuoteBOrder = theme.getColor(textBlockQuoteBorder); + if (blockQuoteBOrder) { + content.push(`.monaco-editor .review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`); + } + + const hcBorder = theme.getColor(contrastBorder); + if (hcBorder) { + content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); + content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor { outline: 1px solid ${hcBorder}; }`); + } + + this._styleElement.innerHTML = content.join('\n'); + + // Editor decorations should also be responsive to theme changes + this.setCommentEditorDecorations(); } show(rangeOrPos: IRange | IPosition, heightInLines: number): void { @@ -561,13 +621,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/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index a61caf0eaf1..1c35b032439 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -5,11 +5,13 @@ 'use strict'; import 'vs/css!./media/review'; +import * as nls from 'vs/nls'; import { $ } from 'vs/base/browser/builder'; +import { findFirstInSorted } from 'vs/base/common/arrays'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ICodeEditor, IEditorMouseEvent, IViewZone } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { registerEditorContribution, EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -28,6 +30,7 @@ import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/par import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export const ctxReviewPanelVisible = new RawContextKey('reviewPanelVisible', false); @@ -72,7 +75,7 @@ export class ReviewController implements IEditorContribution { @IModeService private modeService: IModeService, @IModelService private modelService: IModelService, @ICodeEditorService private codeEditorService: ICodeEditorService, - + @IOpenerService private openerService: IOpenerService ) { this.editor = editor; this.globalToDispose = []; @@ -98,7 +101,7 @@ export class ReviewController implements IEditorContribution { this._commentInfos.forEach(info => { info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, info.owner, thread, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, info.owner, thread, {}); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); }); @@ -154,6 +157,56 @@ export class ReviewController implements IEditorContribution { } } + public nextCommentThread(): void { + if (!this._commentWidgets.length) { + return; + } + + const after = this.editor.getSelection().getEndPosition(); + const sortedWidgets = this._commentWidgets.sort((a, b) => { + if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) { + return -1; + } + + if (a.commentThread.range.startLineNumber > b.commentThread.range.startLineNumber) { + return 1; + } + + if (a.commentThread.range.startColumn < b.commentThread.range.startColumn) { + return -1; + } + + if (a.commentThread.range.startColumn > b.commentThread.range.startColumn) { + return 1; + } + + return 0; + }); + + let idx = findFirstInSorted(sortedWidgets, widget => { + if (widget.commentThread.range.startLineNumber > after.lineNumber) { + return true; + } + + if (widget.commentThread.range.startLineNumber < after.lineNumber) { + return false; + } + + if (widget.commentThread.range.startColumn > after.column) { + return true; + } + return false; + }); + + if (idx === this._commentWidgets.length) { + this._commentWidgets[0].reveal(); + this.editor.setSelection(this._commentWidgets[0].commentThread.range); + } else { + sortedWidgets[idx].reveal(); + this.editor.setSelection(sortedWidgets[idx].commentThread.range); + } + } + getId(): string { return ID; } @@ -190,6 +243,7 @@ export class ReviewController implements IEditorContribution { this._commentWidgets = []; this.localToDispose.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e))); + this.localToDispose.push(this.editor.onDidBlurEditorText(() => this.onDidBlurEditorText())); this.localToDispose.push(this.editor.onDidChangeModelContent(() => { if (this._newCommentGlyph) { this.editor.removeContentWidget(this._newCommentGlyph); @@ -222,7 +276,7 @@ export class ReviewController implements IEditorContribution { } }); added.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, e.owner, thread, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, e.owner, thread, {}); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); @@ -239,7 +293,7 @@ export class ReviewController implements IEditorContribution { // add new comment this._reviewPanelVisible.set(true); const { replyCommand, ownerId } = newCommentInfo; - this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, ownerId, { + this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, ownerId, { threadId: null, resource: null, comments: [], @@ -264,6 +318,10 @@ export class ReviewController implements IEditorContribution { return; } + if (!this.editor.hasTextFocus()) { + return; + } + const hasCommentingRanges = this._commentInfos.length && this._commentInfos.some(info => !!info.commentingRanges.length); if (hasCommentingRanges && e.target.position && e.target.position.lineNumber !== undefined) { if (this._newCommentGlyph && e.target.element.className !== 'comment-hint') { @@ -285,6 +343,12 @@ export class ReviewController implements IEditorContribution { } } + private onDidBlurEditorText(): void { + if (this._newCommentGlyph) { + this.editor.removeContentWidget(this._newCommentGlyph); + } + } + private getNewCommentAction(line: number): { replyCommand: modes.Command, ownerId: number } { for (let i = 0; i < this._commentInfos.length; i++) { const commentInfo = this._commentInfos[i]; @@ -334,7 +398,7 @@ export class ReviewController implements IEditorContribution { this._commentInfos.forEach(info => { info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, info.owner, thread, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, info.owner, thread, {}); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); }); @@ -359,8 +423,27 @@ export class ReviewController implements IEditorContribution { } } -registerEditorContribution(ReviewController); +export class NextCommentThreadAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.nextCommentThreadAction', + label: nls.localize('nextCommentThreadAction', "Go to Next Comment Thread"), + alias: 'Go to Next Comment Thread', + precondition: null, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + let controller = ReviewController.get(editor); + if (controller) { + controller.nextCommentThread(); + } + } +} + +registerEditorContribution(ReviewController); +registerEditorAction(NextCommentThreadAction); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeReviewPanel', diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts index 5e10e22fbb2..c916bec1da8 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts @@ -22,6 +22,8 @@ import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'v import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { textLinkForeground, textLinkActiveForeground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; export const COMMENTS_PANEL_TITLE = 'Comments'; @@ -39,6 +41,7 @@ export class CommentsPanel extends Panel { @ICommentService private commentService: ICommentService, @IEditorService private editorService: IEditorService, @ICommandService private commandService: ICommandService, + @IOpenerService private openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService ) { @@ -60,9 +63,37 @@ export class CommentsPanel extends Panel { this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this); this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this); + const styleElement = dom.createStyleSheet(parent); + this.applyStyles(styleElement); + this.themeService.onThemeChange(_ => { + this.applyStyles(styleElement); + }); + return this.render(); } + private applyStyles(styleElement: HTMLStyleElement) { + const content: string[] = []; + + const theme = this.themeService.getTheme(); + const linkColor = theme.getColor(textLinkForeground); + if (linkColor) { + content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`); + } + + const linkActiveColor = theme.getColor(textLinkActiveForeground); + if (linkActiveColor) { + content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`); + } + + const focusColor = theme.getColor(focusBorder); + if (focusColor) { + content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`); + } + + styleElement.innerHTML = content.join('\n'); + } + private render(): TPromise { dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads()); return this.tree.setInput(this.commentsModel).then(() => { @@ -101,7 +132,7 @@ export class CommentsPanel extends Panel { private createTree(): void { this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { dataSource: new CommentsDataSource(), - renderer: new CommentsModelRenderer(this.instantiationService), + renderer: new CommentsModelRenderer(this.instantiationService, this.openerService), accessibilityProvider: new DefaultAccessibilityProvider, controller: new DefaultController(), dnd: new DefaultDragAndDrop(), diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts b/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts index df16d651425..d8672d23d57 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts @@ -5,9 +5,13 @@ import * as dom from 'vs/base/browser/dom'; import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; import { IDataSource, IFilter, IRenderer as ITreeRenderer, ITree } from 'vs/base/parts/tree/browser/tree'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/parts/comments/common/commentModel'; @@ -59,6 +63,7 @@ interface ICommentThreadTemplateData { icon: HTMLImageElement; userName: HTMLSpanElement; commentText: HTMLElement; + disposables: Disposable[]; } export class CommentsModelRenderer implements ITreeRenderer { @@ -67,7 +72,8 @@ export class CommentsModelRenderer implements ITreeRenderer { constructor( - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService ) { } @@ -99,6 +105,10 @@ export class CommentsModelRenderer implements ITreeRenderer { switch (templateId) { case CommentsModelRenderer.RESOURCE_ID: (templateData).resourceLabel.dispose(); + break; + case CommentsModelRenderer.COMMENT_ID: + (templateData).disposables.forEach(disposeable => disposeable.dispose()); + break; } } @@ -124,6 +134,7 @@ export class CommentsModelRenderer implements ITreeRenderer { const labelContainer = dom.append(container, dom.$('.comment-container')); data.userName = dom.append(labelContainer, dom.$('.user')); data.commentText = dom.append(labelContainer, dom.$('.text')); + data.disposables = []; return data; } @@ -134,7 +145,18 @@ export class CommentsModelRenderer implements ITreeRenderer { private renderCommentElement(tree: ITree, element: CommentNode, templateData: ICommentThreadTemplateData) { templateData.userName.textContent = element.comment.userName; - templateData.commentText.innerHTML = renderMarkdown(element.comment.body, { inline: true }).innerHTML; + templateData.commentText.innerHTML = ''; + const renderedComment = renderMarkdown(element.comment.body, { + inline: true, + actionHandler: { + callback: (content) => { + this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError); + }, + disposeables: templateData.disposables + } + }); + + templateData.commentText.appendChild(renderedComment); } } diff --git a/src/vs/workbench/parts/comments/electron-browser/media/review.css b/src/vs/workbench/parts/comments/electron-browser/media/review.css index 9854ade1898..dd28fb338ef 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/review.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/review.css @@ -11,10 +11,7 @@ } .monaco-editor .comment-hint{ - display: flex; - align-items: center; - justify-content: center; - height: 16px; + height: 20px; width: 20px; padding-left: 2px; background: url('comment.svg') center center no-repeat; @@ -38,6 +35,13 @@ display: flex; } +.monaco-editor .review-widget .body .review-comment blockquote { + margin: 0 7px 0 5px; + padding: 0 16px 0 10px; + border-left-width: 5px; + border-left-style: solid; +} + .monaco-editor .review-widget .body .review-comment .avatar-container { margin-top: 4px !important; } @@ -55,6 +59,7 @@ .monaco-editor .review-widget .body .review-comment .review-comment-contents { margin-left: 20px; + user-select: text; } .monaco-editor .review-widget .body pre { @@ -144,6 +149,7 @@ white-space: nowrap; border: 0px; cursor: text; + outline: 1px solid transparent; } .monaco-editor .review-widget .body .comment-form .review-thread-reply-button:focus { diff --git a/src/vs/workbench/parts/debug/browser/baseDebugView.ts b/src/vs/workbench/parts/debug/browser/baseDebugView.ts index e3cc797de24..54f3838448a 100644 --- a/src/vs/workbench/parts/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/parts/debug/browser/baseDebugView.ts @@ -83,16 +83,16 @@ export function renderExpressionValue(expressionOrValue: IExpression | string, c } } - if (options.maxValueLength && value.length > options.maxValueLength) { + if (options.maxValueLength && value && value.length > options.maxValueLength) { value = value.substr(0, options.maxValueLength) + '...'; } if (value && !options.preserveWhitespace) { container.textContent = replaceWhitespace(value); } else { - container.textContent = value; + container.textContent = value || ''; } if (options.showHover) { - container.title = value; + container.title = value || ''; } } @@ -103,18 +103,15 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData, dom.toggleClass(data.name, 'virtual', !!variable.presentationHint && variable.presentationHint.kind === 'virtual'); } - if (variable.value) { - data.name.textContent += (typeof variable.name === 'string') ? ':' : ''; - renderExpressionValue(variable, data.value, { - showChanged, - maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - preserveWhitespace: false, - showHover: true, - colorize: true - }); - } else { - data.value.textContent = ''; - data.value.title = ''; + renderExpressionValue(variable, data.value, { + showChanged, + maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, + preserveWhitespace: false, + showHover: true, + colorize: true + }); + if (variable.value && typeof variable.name === 'string') { + data.name.textContent += ':'; } } diff --git a/src/vs/workbench/parts/debug/browser/breakpointsView.ts b/src/vs/workbench/parts/debug/browser/breakpointsView.ts index 77f2cd56085..edf399c0e2a 100644 --- a/src/vs/workbench/parts/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/browser/breakpointsView.ts @@ -23,7 +23,7 @@ import { basename } from 'vs/base/common/paths'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IDelegate, IListContextMenuEvent, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IListContextMenuEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { IEditor } from 'vs/workbench/common/editor'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -221,7 +221,7 @@ export class BreakpointsView extends ViewletPanel { } } -class BreakpointsDelegate implements IDelegate { +class BreakpointsDelegate implements IVirtualDelegate { constructor(private debugService: IDebugService) { // noop @@ -338,6 +338,10 @@ class BreakpointsRenderer implements IRenderer { const debugService = accessor.get(IDebugService); const editorService = accessor.get(IEditorService); @@ -221,23 +223,23 @@ export function registerCommands(): void { weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), primary: KeyMod.Shift | KeyCode.F9, when: EditorContextKeys.editorTextFocus, - id: INLINE_BREAKPOINT_COMMAND_ID, + id: TOGGLE_INLINE_BREAKPOINT_ID, handler: inlineBreakpointHandler }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { - id: INLINE_BREAKPOINT_COMMAND_ID, + id: TOGGLE_INLINE_BREAKPOINT_ID, title: nls.localize('inlineBreakpoint', "Inline Breakpoint"), category: nls.localize('debug', "Debug") } }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { command: { - id: INLINE_BREAKPOINT_COMMAND_ID, + id: TOGGLE_INLINE_BREAKPOINT_ID, title: nls.localize('addInlineBreakpoint', "Add Inline Breakpoint") }, - when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, EditorContextKeys.writable), + when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, EditorContextKeys.writable, EditorContextKeys.editorTextFocus), group: 'debug', order: 1 }); diff --git a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts index 4c871d8342b..a8e56c2bc4c 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts @@ -10,17 +10,18 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint } from 'vs/workbench/parts/debug/common/debug'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/parts/debug/browser/breakpointsView'; +export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; class ToggleBreakpointAction extends EditorAction { constructor() { super({ - id: 'editor.debug.action.toggleBreakpoint', + id: TOGGLE_BREAKPOINT_ID, label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), alias: 'Debug: Toggle Breakpoint', precondition: null, @@ -49,11 +50,12 @@ class ToggleBreakpointAction extends EditorAction { } } +export const TOGGLE_CONDITIONAL_BREAKPOINT_ID = 'editor.debug.action.conditionalBreakpoint'; class ConditionalBreakpointAction extends EditorAction { constructor() { super({ - id: 'editor.debug.action.conditionalBreakpoint', + id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), alias: 'Debug: Add Conditional Breakpoint...', precondition: null @@ -70,11 +72,12 @@ class ConditionalBreakpointAction extends EditorAction { } } +export const TOGGLE_LOG_POINT_ID = 'editor.debug.action.toggleLogPoint'; class LogPointAction extends EditorAction { constructor() { super({ - id: 'editor.debug.action.toggleLogPoint', + id: TOGGLE_LOG_POINT_ID, label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), alias: 'Debug: Add Logpoint...', precondition: null @@ -98,7 +101,7 @@ class RunToCursorAction extends EditorAction { id: 'editor.debug.action.runToCursor', label: nls.localize('runToCursor', "Run to Cursor"), alias: 'Debug: Run to Cursor', - precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, EditorContextKeys.writable, CONTEXT_DEBUG_STATE.isEqualTo('stopped')), + precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, EditorContextKeys.writable, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), EditorContextKeys.editorTextFocus), menuOpts: { group: 'debug', order: 2 @@ -141,7 +144,7 @@ class SelectionToReplAction extends EditorAction { id: 'editor.debug.action.selectionToRepl', label: nls.localize('debugEvaluate', "Debug: Evaluate"), alias: 'Debug: Evaluate', - precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL), + precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), menuOpts: { group: 'debug', order: 0 @@ -167,7 +170,7 @@ class SelectionToWatchExpressionsAction extends EditorAction { id: 'editor.debug.action.selectionToWatch', label: nls.localize('debugAddToWatch', "Debug: Add to Watch"), alias: 'Debug: Add to Watch', - precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL), + precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), menuOpts: { group: 'debug', order: 1 diff --git a/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts new file mode 100644 index 00000000000..da803391a97 --- /dev/null +++ b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { TreeViewsViewletPanel, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as dom from 'vs/base/browser/dom'; +import { IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { renderViewTree, twistiePixels } from 'vs/workbench/parts/debug/browser/baseDebugView'; +import { IAccessibilityProvider, ITree, IRenderer, IDataSource } from 'vs/base/parts/tree/browser/tree'; + +export class LoadedScriptsView extends TreeViewsViewletPanel { + + private treeContainer: HTMLElement; + + constructor( + options: IViewletViewOptions, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService); + } + + protected renderBody(container: HTMLElement): void { + dom.addClass(container, 'debug-loaded-scripts'); + this.treeContainer = renderViewTree(container); + + this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { + dataSource: new LoadedScriptsDataSource(), + renderer: this.instantiationService.createInstance(LoadedScriptsRenderer), + accessibilityProvider: new LoadedSciptsAccessibilityProvider(), + }, { + ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts"), + twistiePixels + }); + } + + layoutBody(size: number): void { + if (this.treeContainer) { + this.treeContainer.style.height = size + 'px'; + } + super.layoutBody(size); + } +} + +// A good example of data source, renderers, action providers and accessibilty providers can be found in the callStackView.ts + +class LoadedScriptsDataSource implements IDataSource { + + getId(tree: ITree, element: any): string { + throw new Error('Method not implemented.'); + } + + hasChildren(tree: ITree, element: any): boolean { + throw new Error('Method not implemented.'); + } + + getChildren(tree: ITree, element: any): TPromise { + throw new Error('Method not implemented.'); + } + + getParent(tree: ITree, element: any): TPromise { + throw new Error('Method not implemented.'); + } +} + +class LoadedScriptsRenderer implements IRenderer { + + getHeight(tree: ITree, element: any): number { + throw new Error('Method not implemented.'); + } + + getTemplateId(tree: ITree, element: any): string { + throw new Error('Method not implemented.'); + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + throw new Error('Method not implemented.'); + } + + renderElement(tree: ITree, element: any, templateId: string, templateData: any): void { + throw new Error('Method not implemented.'); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: any): void { + throw new Error('Method not implemented.'); + } +} + +class LoadedSciptsAccessibilityProvider implements IAccessibilityProvider { + + public getAriaLabel(tree: ITree, element: any): string { + return nls.localize('implement me', "implement me"); + } +} diff --git a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css index ea156779fae..09cf7033e9a 100644 --- a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css @@ -372,6 +372,7 @@ .debug-viewlet .debug-breakpoints .breakpoint > .icon { width: 19px; height: 19px; + min-width: 19px; } .debug-viewlet .debug-breakpoints .breakpoint > .file-path { @@ -383,6 +384,11 @@ overflow: hidden; } +.debug-viewlet .debug-breakpoints .breakpoint .name { + overflow: hidden; + text-overflow: ellipsis +} + .debug-viewlet .debug-action.remove { background: url('remove.svg') center center no-repeat; } diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 16fb543aa5f..8db643264cd 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -17,7 +17,7 @@ import { Position } from 'vs/editor/common/core/position'; import { ISuggestion } from 'vs/editor/common/modes'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { Range, IRange } from 'vs/editor/common/core/range'; -import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -31,15 +31,15 @@ export const VIEW_CONTAINER: ViewContainer = Registry.as('debugType', undefined); -export const CONTEXT_DEBUG_STATE = new RawContextKey('debugState', undefined); -export const CONTEXT_IN_DEBUG_MODE = new RawContextKey('inDebugMode', false); -export const CONTEXT_NOT_IN_DEBUG_MODE: ContextKeyExpr = CONTEXT_IN_DEBUG_MODE.toNegated(); +export const CONTEXT_DEBUG_STATE = new RawContextKey('debugState', 'inactive'); +export const CONTEXT_NOT_IN_DEBUG_MODE = CONTEXT_DEBUG_STATE.isEqualTo('inactive'); +export const CONTEXT_IN_DEBUG_MODE = CONTEXT_DEBUG_STATE.notEqualsTo('inactive'); export const CONTEXT_IN_DEBUG_REPL = new RawContextKey('inDebugRepl', false); -export const CONTEXT_NOT_IN_DEBUG_REPL: ContextKeyExpr = CONTEXT_IN_DEBUG_REPL.toNegated(); export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('breakpointWidgetVisible', false); export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true); @@ -48,6 +48,7 @@ export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFo export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false); export const CONTEXT_BREAKPOINT_SELECTED = new RawContextKey('breakpointSelected', false); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined); +export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const DEBUG_SCHEME = 'debug'; @@ -115,7 +116,7 @@ export interface IRawSession { evaluate(args: DebugProtocol.EvaluateArguments): TPromise; readonly capabilities: DebugProtocol.Capabilities; - disconnect(restart?: boolean, force?: boolean): TPromise; + terminate(restart?: boolean): TPromise; custom(request: string, args: any): TPromise; onDidEvent: Event; onDidInitialize: Event; diff --git a/src/vs/workbench/parts/debug/common/debugProtocol.d.ts b/src/vs/workbench/parts/debug/common/debugProtocol.d.ts index 1e7e0cad704..70dc4264dfa 100644 --- a/src/vs/workbench/parts/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/parts/debug/common/debugProtocol.d.ts @@ -429,6 +429,22 @@ declare module DebugProtocol { export interface DisconnectResponse extends Response { } + /** Terminate request; value of command field is 'terminate'. + The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance to terminate itself. + */ + export interface TerminateRequest extends Request { + // command: 'terminate'; + arguments?: TerminateArguments; + } + + /** Arguments for 'terminate' request. */ + export interface TerminateArguments { + } + + /** Response to 'terminate' request. This is just an acknowledgement, so no body field is required. */ + export interface TerminateResponse extends Response { + } + /** SetBreakpoints request; value of command field is 'setBreakpoints'. Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. To clear all breakpoint for a source, specify an empty array. @@ -1176,6 +1192,8 @@ declare module DebugProtocol { supportsTerminateThreadsRequest?: boolean; /** The debug adapter supports the 'setExpression' request. */ supportsSetExpression?: boolean; + /** The debug adapter supports the 'terminate' request. */ + supportsTerminateRequest?: boolean; } /** An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with. */ diff --git a/src/vs/workbench/parts/debug/common/debugViewModel.ts b/src/vs/workbench/parts/debug/common/debugViewModel.ts index 7c945318b01..d1d7cc88b50 100644 --- a/src/vs/workbench/parts/debug/common/debugViewModel.ts +++ b/src/vs/workbench/parts/debug/common/debugViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, ISession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED } from 'vs/workbench/parts/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, ISession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED } from 'vs/workbench/parts/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; export class ViewModel implements IViewModel { @@ -20,6 +20,7 @@ export class ViewModel implements IViewModel { private multiSessionView: boolean; private expressionSelectedContextKey: IContextKey; private breakpointSelectedContextKey: IContextKey; + private loadedScriptsSupportedContextKey: IContextKey; constructor(contextKeyService: IContextKeyService) { this._onDidFocusSession = new Emitter(); @@ -28,6 +29,7 @@ export class ViewModel implements IViewModel { this.multiSessionView = false; this.expressionSelectedContextKey = CONTEXT_EXPRESSION_SELECTED.bindTo(contextKeyService); this.breakpointSelectedContextKey = CONTEXT_BREAKPOINT_SELECTED.bindTo(contextKeyService); + this.loadedScriptsSupportedContextKey = CONTEXT_LOADED_SCRIPTS_SUPPORTED.bindTo(contextKeyService); } public getId(): string { @@ -66,6 +68,10 @@ export class ViewModel implements IViewModel { this._focusedThread = thread; this._focusedStackFrame = stackFrame; + this.loadedScriptsSupportedContextKey.set(session && session.raw.capabilities.supportsLoadedSourcesRequest); + // @weinand remove the next line which always disables the context for the view to be shown + this.loadedScriptsSupportedContextKey.set(false); + if (shouldEmit) { this._onDidFocusStackFrame.fire({ stackFrame, explicit }); } diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts index 69c19b227fc..b6781653be4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts @@ -127,7 +127,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi protected _fillContainer(container: HTMLElement): void { this.setCssClass('breakpoint-widget'); - const selectBox = new SelectBox([nls.localize('expression', "Expression"), nls.localize('hitCount', "Hit Count"), nls.localize('logMessage', "Log Message")], this.context, this.contextViewService); + const selectBox = new SelectBox([nls.localize('expression', "Expression"), nls.localize('hitCount', "Hit Count"), nls.localize('logMessage', "Log Message")], this.context, this.contextViewService, null, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); this.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService)); this.selectContainer = $('.breakpoint-select-container'); selectBox.render(dom.append(container, this.selectContainer)); @@ -203,7 +203,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const codeEditorWidgetOptions = SimpleDebugEditor.getCodeEditorWidgetOptions(); this.input = scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions); CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true); - const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:breakpointinput`), true); + const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true); this.input.setModel(model); this.toDispose.push(model); const setDecorations = () => { diff --git a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts index 8b626653218..3868462dbe7 100644 --- a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts @@ -88,10 +88,10 @@ export class CallStackView extends TreeViewsViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const title = dom.append(container, $('.title.debug-call-stack-title')); - const name = dom.append(title, $('span')); - name.textContent = this.options.title; - this.pauseMessage = dom.append(title, $('span.pause-message')); + let titleContainer = dom.append(container, $('.debug-call-stack-title')); + super.renderHeaderTitle(titleContainer, this.options.title); + + this.pauseMessage = dom.append(titleContainer, $('span.pause-message')); this.pauseMessage.hidden = true; this.pauseMessageLabel = dom.append(this.pauseMessage, $('span.label')); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index a0dd24849eb..a9b45955e1f 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -23,21 +23,21 @@ import { CallStackView } from 'vs/workbench/parts/debug/electron-browser/callSta import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, REPL_ID, CONTEXT_NOT_IN_DEBUG_MODE, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, VIEW_CONTAINER + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, VIEW_CONTAINER, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED } from 'vs/workbench/parts/debug/common/debug'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { DebugEditorModelManager } from 'vs/workbench/parts/debug/browser/debugEditorModelManager'; import { StepOverAction, ClearReplAction, FocusReplAction, StepIntoAction, StepOutAction, StartAction, RestartAction, ContinueAction, StopAction, DisconnectAction, PauseAction, AddFunctionBreakpointAction, - ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction, TerminateThreadAction + ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction, TerminateThreadAction, ToggleReplAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { DebugActionsWidget } from 'vs/workbench/parts/debug/browser/debugActionsWidget'; import * as service from 'vs/workbench/parts/debug/electron-browser/debugService'; import { DebugContentProvider } from 'vs/workbench/parts/debug/browser/debugContentProvider'; import 'vs/workbench/parts/debug/electron-browser/debugEditorContribution'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { registerCommands } from 'vs/workbench/parts/debug/browser/debugCommands'; +import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID } from 'vs/workbench/parts/debug/browser/debugCommands'; import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { StatusBarColorProvider } from 'vs/workbench/parts/debug/browser/statusbarColorProvider'; import { ViewsRegistry } from 'vs/workbench/common/views'; @@ -51,6 +51,8 @@ import { DebugStatus } from 'vs/workbench/parts/debug/browser/debugStatus'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { LoadedScriptsView } from 'vs/workbench/parts/debug/browser/loadedScriptsView'; +import { TOGGLE_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID } from 'vs/workbench/parts/debug/browser/debugEditorActions'; class OpenDebugViewletAction extends ToggleViewletAction { public static readonly ID = VIEWLET_ID; @@ -111,6 +113,7 @@ Registry.as(PanelExtensions.Panels).setDefaultPanelId(REPL_ID); ViewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctor: VariablesView, order: 10, weight: 40, container: VIEW_CONTAINER, canToggleVisibility: true }]); ViewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctor: WatchExpressionsView, order: 20, weight: 10, container: VIEW_CONTAINER, canToggleVisibility: true }]); ViewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctor: CallStackView, order: 30, weight: 30, container: VIEW_CONTAINER, canToggleVisibility: true }]); +ViewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctor: LoadedScriptsView, order: 35, weight: 10, container: VIEW_CONTAINER, canToggleVisibility: true, when: CONTEXT_LOADED_SCRIPTS_SUPPORTED }]); ViewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctor: BreakpointsView, order: 40, weight: 20, container: VIEW_CONTAINER, canToggleVisibility: true }]); // register action to open viewlet @@ -225,6 +228,200 @@ registerCommands(); const statusBar = Registry.as(StatusExtensions.Statusbar); statusBar.registerStatusbarItem(new StatusbarItemDescriptor(DebugStatus, StatusbarAlignment.LEFT, 30 /* Low Priority */)); +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: nls.localize({ key: 'miViewDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: ToggleReplAction.ID, + title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") + }, + order: 2 +}); + +// Debug menu + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: StartAction.ID, + title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: RunAction.ID, + title: nls.localize({ key: 'miStartWithoutDebugging', comment: ['&& denotes a mnemonic'] }, "Start &&Without Debugging") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: StopAction.ID, + title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging"), + precondition: CONTEXT_IN_DEBUG_MODE + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: RestartAction.ID, + title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging"), + precondition: CONTEXT_IN_DEBUG_MODE + }, + order: 4 +}); + +// Configuration +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: ConfigureAction.ID, + title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: ADD_CONFIGURATION_ID, + title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "Add Configuration...") + }, + order: 2 +}); + +// Step Commands +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: StepOverAction.ID, + title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: StepIntoAction.ID, + title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: StepOutAction.ID, + title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: ContinueAction.ID, + title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 4 +}); + +// New Breakpoints +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: TOGGLE_BREAKPOINT_ID, + title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, + title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Conditional Breakpoint...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: TOGGLE_INLINE_BREAKPOINT_ID, + title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle Inline Breakp&&oint") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: AddFunctionBreakpointAction.ID, + title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Function Breakpoint...") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: TOGGLE_LOG_POINT_ID, + title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Logpoint...") + }, + order: 5 +}); + +// Modify Breakpoints +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: EnableAllBreakpointsAction.ID, + title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Enable All Breakpoints") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: DisableAllBreakpointsAction.ID, + title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: RemoveAllBreakpointsAction.ID, + title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + order: 3 +}); + // Touch Bar if (isMacintosh) { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 992050fd338..f5f797f7475 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -47,6 +47,7 @@ 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'; +import { CancellationToken } from 'vs/base/common/cancellation'; const HOVER_DELAY = 300; const LAUNCH_JSON_REGEX = /launch\.json$/; @@ -379,7 +380,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { @memoize private get provideNonDebugHoverScheduler(): RunOnceScheduler { const scheduler = new RunOnceScheduler(() => { - getHover(this.editor.getModel(), this.nonDebugHoverPosition); + getHover(this.editor.getModel(), this.nonDebugHoverPosition, CancellationToken.None); }, HOVER_DELAY); this.toDispose.push(scheduler); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 89d5e03c9ed..637e1aed9df 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -76,7 +76,6 @@ export class DebugService implements debug.IDebugService { private configurationManager: ConfigurationManager; private toDispose: lifecycle.IDisposable[]; private toDisposeOnSessionEnd: Map; - private inDebugMode: IContextKey; private debugType: IContextKey; private debugState: IContextKey; private breakpointsToSendOnResourceSaved: Set; @@ -120,7 +119,6 @@ export class DebugService implements debug.IDebugService { this.configurationManager = this.instantiationService.createInstance(ConfigurationManager); this.toDispose.push(this.configurationManager); - this.inDebugMode = debug.CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.debugType = debug.CONTEXT_DEBUG_TYPE.bindTo(contextKeyService); this.debugState = debug.CONTEXT_DEBUG_STATE.bindTo(contextKeyService); @@ -167,15 +165,16 @@ export class DebugService implements debug.IDebugService { (session.raw).attach(session.configuration); }); } else { - this.onRawSessionEnd(raw); - this.doCreateSession(session.raw.root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId()); + const root = raw.root; + raw.dispose(); + this.doCreateSession(root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId()); } return; } if (broadcast.channel === EXTENSION_TERMINATE_BROADCAST_CHANNEL) { - this.onRawSessionEnd(raw); + raw.terminate().done(undefined, errors.onUnexpectedError); return; } @@ -285,8 +284,6 @@ export class DebugService implements debug.IDebugService { } private registerSessionListeners(session: debug.ISession, raw: RawDebugSession): void { - this.toDisposeOnSessionEnd.get(raw.getId()).push(raw); - this.toDisposeOnSessionEnd.get(raw.getId()).push(raw.onDidInitialize(event => { aria.status(nls.localize('debuggingStarted', "Debugging started.")); const sendConfigurationDone = () => { @@ -294,7 +291,7 @@ export class DebugService implements debug.IDebugService { return raw.configurationDone().done(null, e => { // Disconnect the debug session on configuration done error #10596 if (raw) { - raw.disconnect().done(null, errors.onUnexpectedError); + raw.dispose(); } this.notificationService.error(e.message); }); @@ -344,7 +341,7 @@ export class DebugService implements debug.IDebugService { if (event.body && event.body.restart && session) { this.restartSession(session, event.body.restart).done(null, err => this.notificationService.error(err.message)); } else { - raw.disconnect().done(null, errors.onUnexpectedError); + raw.dispose(); } } })); @@ -447,7 +444,7 @@ export class DebugService implements debug.IDebugService { }); } if (session && session.getId() === event.sessionId) { - this.onRawSessionEnd(raw); + this.onExitAdapter(raw); } })); @@ -910,7 +907,6 @@ export class DebugService implements debug.IDebugService { const resolved = configuration.resolved; resolved.__sessionId = sessionId; - this.inDebugMode.set(true); const dbg = this.configurationManager.getDebugger(resolved.type); return this.initializeRawSession(root, configuration, sessionId).then(session => { @@ -977,7 +973,7 @@ export class DebugService implements debug.IDebugService { this.telemetryService.publicLog('debugMisconfiguration', { type: resolved ? resolved.type : undefined, error: errorMessage }); this.updateStateAndEmit(raw.getId(), debug.State.Inactive); if (!raw.disconnected) { - raw.disconnect().done(null, errors.onUnexpectedError); + raw.dispose(); } else if (session) { this.model.removeSession(session.getId()); } @@ -986,9 +982,6 @@ export class DebugService implements debug.IDebugService { if (this.model.getReplElements().length > 0) { this.panelService.openPanel(debug.REPL_ID, false).done(undefined, errors.onUnexpectedError); } - if (this.model.getReplElements().length === 0) { - this.inDebugMode.reset(); - } this.showError(errorMessage, errors.isErrorWithActions(error) ? error.actions : []); return undefined; @@ -1117,7 +1110,7 @@ export class DebugService implements debug.IDebugService { // Do not run preLaunch and postDebug tasks for automatic restarts this.skipRunningTask = !!restartData; - return session.raw.disconnect(true).then(() => { + return session.raw.terminate(true).then(() => { if (strings.equalsIgnoreCase(session.configuration.type, 'extensionHost') && session.raw.root) { return this.broadcastService.broadcast({ channel: EXTENSION_RELOAD_BROADCAST_CHANNEL, @@ -1159,12 +1152,12 @@ export class DebugService implements debug.IDebugService { public stopSession(session: debug.ISession): TPromise { if (session) { - return session.raw.disconnect(false, true); + return session.raw.terminate(); } const sessions = this.model.getSessions(); if (sessions.length) { - return TPromise.join(sessions.map(s => s.raw.disconnect(false, true))); + return TPromise.join(sessions.map(s => s.raw.terminate(false))); } this.sessionStates.clear(); @@ -1172,7 +1165,7 @@ export class DebugService implements debug.IDebugService { return undefined; } - private onRawSessionEnd(raw: RawDebugSession): void { + private onExitAdapter(raw: RawDebugSession): void { const breakpoints = this.model.getBreakpoints(); const session = this.model.getSessions().filter(p => p.getId() === raw.getId()).pop(); /* __GDPR__ @@ -1210,7 +1203,6 @@ export class DebugService implements debug.IDebugService { this.updateStateAndEmit(raw.getId(), debug.State.Inactive); if (this.model.getSessions().length === 0) { - this.inDebugMode.reset(); this.debugType.reset(); this.viewModel.setMultiSessionView(false); diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 645efdd8418..a2bf801d4cc 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -10,32 +10,32 @@ import { Action } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import * as debug from 'vs/workbench/parts/debug/common/debug'; import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { formatPII } from 'vs/workbench/parts/debug/common/debugUtils'; import { SocketDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { SessionState, DebugEvent, IRawSession, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; -export interface SessionExitedEvent extends debug.DebugEvent { +export interface SessionExitedEvent extends DebugEvent { body: { exitCode: number, sessionId: string }; } -export interface SessionTerminatedEvent extends debug.DebugEvent { +export interface SessionTerminatedEvent extends DebugEvent { body: { restart?: boolean, sessionId: string }; } -export class RawDebugSession implements debug.IRawSession { +export class RawDebugSession implements IRawSession { - private debugAdapter: debug.IDebugAdapter; + private debugAdapter: IDebugAdapter; public emittedStopped: boolean; public readyForBreakpoints: boolean; @@ -43,9 +43,11 @@ export class RawDebugSession implements debug.IRawSession { private cachedInitServerP: TPromise; private startTime: number; public disconnected: boolean; + private terminated: boolean; private sentPromises: TPromise[]; private _capabilities: DebugProtocol.Capabilities; private allThreadsContinued: boolean; + private state: SessionState = SessionState.LAUNCH; private readonly _onDidInitialize: Emitter; private readonly _onDidStop: Emitter; @@ -56,7 +58,7 @@ export class RawDebugSession implements debug.IRawSession { private readonly _onDidThread: Emitter; private readonly _onDidOutput: Emitter; private readonly _onDidBreakpoint: Emitter; - private readonly _onDidCustomEvent: Emitter; + private readonly _onDidCustomEvent: Emitter; private readonly _onDidEvent: Emitter; constructor( @@ -83,7 +85,7 @@ export class RawDebugSession implements debug.IRawSession { this._onDidThread = new Emitter(); this._onDidOutput = new Emitter(); this._onDidBreakpoint = new Emitter(); - this._onDidCustomEvent = new Emitter(); + this._onDidCustomEvent = new Emitter(); this._onDidEvent = new Emitter(); } @@ -127,7 +129,7 @@ export class RawDebugSession implements debug.IRawSession { return this._onDidBreakpoint.event; } - public get onDidCustomEvent(): Event { + public get onDidCustomEvent(): Event { return this._onDidCustomEvent.event; } @@ -229,7 +231,7 @@ export class RawDebugSession implements debug.IRawSession { }, () => errorCallback(errors.canceled())); } - private onDapEvent(event: debug.DebugEvent): void { + private onDapEvent(event: DebugEvent): void { event.sessionId = this.id; if (event.event === 'initialized') { @@ -282,6 +284,7 @@ export class RawDebugSession implements debug.IRawSession { } public attach(args: DebugProtocol.AttachRequestArguments): TPromise { + this.state = SessionState.ATTACH; return this.send('attach', args).then(response => this.readCapabilities(response)); } @@ -339,24 +342,13 @@ export class RawDebugSession implements debug.IRawSession { return this.send('completions', args); } - public disconnect(restart = false, force = false): TPromise { - if (this.disconnected && force) { - return this.stopServer(); - } - - // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. - // Give a 1s timeout to give a chance for some promises to complete. - setTimeout(() => { - this.sentPromises.forEach(p => p && p.cancel()); - this.sentPromises = []; - }, 1000); - - if (this.debugAdapter && !this.disconnected) { - // point of no return: from now on don't report any errors - this.disconnected = true; - return this.send('disconnect', { restart: restart }, false).then(() => this.stopServer(), () => this.stopServer()); + public terminate(restart = false): TPromise { + if (this.capabilities.supportsTerminateRequest && !this.terminated && this.state === SessionState.LAUNCH) { + this.terminated = true; + return this.send('terminate', { restart }); } + this.dispose(restart); return TPromise.as(null); } @@ -428,43 +420,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); } } @@ -480,6 +475,26 @@ export class RawDebugSession implements debug.IRawSession { }); } + public dispose(restart = false): void { + if (this.disconnected) { + this.stopServer().done(undefined, errors.onUnexpectedError); + } else { + + // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. + // Give a 1s timeout to give a chance for some promises to complete. + setTimeout(() => { + this.sentPromises.forEach(p => p && p.cancel()); + this.sentPromises = []; + }, 1000); + + if (this.debugAdapter && !this.disconnected) { + // point of no return: from now on don't report any errors + this.disconnected = true; + this.send('disconnect', { restart }, false).then(() => this.stopServer(), () => this.stopServer()).done(undefined, errors.onUnexpectedError); + } + } + } + private stopServer(): TPromise { if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) { @@ -510,8 +525,4 @@ export class RawDebugSession implements debug.IRawSession { } this._onDidExitAdapter.fire({ sessionId: this.getId() }); } - - public dispose(): void { - this.disconnect().done(null, errors.onUnexpectedError); - } } diff --git a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts index 5a517047652..3ff99dc0765 100644 --- a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts @@ -300,16 +300,17 @@ class WatchExpressionsRenderer implements IRenderer { } data.name.textContent = watchExpression.name; - if (watchExpression.value) { + renderExpressionValue(watchExpression, data.value, { + showChanged: true, + maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, + preserveWhitespace: false, + showHover: true, + colorize: true + }); + data.name.title = watchExpression.type ? watchExpression.type : watchExpression.value; + + if (typeof watchExpression.value === 'string') { data.name.textContent += ':'; - renderExpressionValue(watchExpression, data.value, { - showChanged: true, - maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - preserveWhitespace: false, - showHover: true, - colorize: true - }); - data.name.title = watchExpression.type ? watchExpression.type : watchExpression.value; } } diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index aaa7993b320..ceb4cc25a49 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -186,7 +186,7 @@ export class MockSession implements IRawSession { return TPromise.as(null); } - public disconnect(restart?: boolean, force?: boolean): TPromise { + public terminate(restart = false): TPromise { return TPromise.as(null); } 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..c7b217c4413 100644 --- a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts +++ b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts @@ -40,37 +40,41 @@ if (env.isWindows) { getDefaultTerminalLinuxReady().then(defaultTerminalLinux => { let configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ - 'id': 'externalTerminal', - 'order': 100, - 'title': nls.localize('terminalConfigurationTitle', "External Terminal"), - 'type': 'object', - 'properties': { + id: 'externalTerminal', + order: 100, + title: nls.localize('terminalConfigurationTitle', "External Terminal"), + type: 'object', + properties: { 'terminal.explorerKind': { - 'type': 'string', - 'enum': [ + type: 'string', + enum: [ 'integrated', 'external' ], - 'description': nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), - 'default': 'integrated' + enumDescriptions: [ + nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), + nls.localize('terminal.explorerKind.external', "Use the configured external terminal.") + ], + description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), + default: 'integrated' }, 'terminal.external.windowsExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), - 'default': getDefaultTerminalWindows(), - 'scope': ConfigurationScope.APPLICATION + type: 'string', + description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), + default: getDefaultTerminalWindows(), + scope: ConfigurationScope.APPLICATION }, 'terminal.external.osxExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on OS X."), - 'default': DEFAULT_TERMINAL_OSX, - 'scope': ConfigurationScope.APPLICATION + type: 'string', + description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."), + default: DEFAULT_TERMINAL_OSX, + scope: ConfigurationScope.APPLICATION }, 'terminal.external.linuxExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), - 'default': defaultTerminalLinux, - 'scope': ConfigurationScope.APPLICATION + type: 'string', + description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), + default: defaultTerminalLinux, + scope: ConfigurationScope.APPLICATION } } }); @@ -129,15 +133,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/experiments/electron-browser/experimentalPrompt.ts b/src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts index 9c59b4b6784..cee00f64590 100644 --- a/src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts +++ b/src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts @@ -5,7 +5,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; -import { IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties, IExperimentActionPromptCommand, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -57,7 +57,7 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib actionProperties.commands = []; } - const choices: IPromptChoice[] = actionProperties.commands.map(command => { + const choices: IPromptChoice[] = actionProperties.commands.map((command: IExperimentActionPromptCommand) => { return { label: command.text, run: () => { diff --git a/src/vs/workbench/parts/experiments/node/experimentService.ts b/src/vs/workbench/parts/experiments/node/experimentService.ts index 66d54a3904b..ea5ff884fe7 100644 --- a/src/vs/workbench/parts/experiments/node/experimentService.ts +++ b/src/vs/workbench/parts/experiments/node/experimentService.ts @@ -55,9 +55,24 @@ interface IRawExperiment { workspaceExcludes?: string[]; minEditCount: number; }, + experimentsPreviouslyRun?: { + excludes?: string[]; + includes?: string[]; + } userProbability?: number; }; - action?: { type: string; properties: any }; + action?: IExperimentAction; +} + +interface IExperimentAction { + type: ExperimentActionType; + properties: any; +} + +export enum ExperimentActionType { + Custom = 'Custom', + Prompt = 'Prompt', + AddToRecommendations = 'AddToRecommendations' } export interface IExperimentActionPromptProperties { @@ -65,10 +80,9 @@ export interface IExperimentActionPromptProperties { commands: IExperimentActionPromptCommand[]; } -interface IExperimentActionPromptCommand { +export interface IExperimentActionPromptCommand { text: string; externalLink?: string; - dontShowAgain?: boolean; curatedExtensionsKey?: string; curatedExtensionsList?: string[]; } @@ -80,21 +94,10 @@ export interface IExperiment { action?: IExperimentAction; } -export enum ExperimentActionType { - Custom, - Prompt, - AddToRecommendations -} - -interface IExperimentAction { - type: ExperimentActionType; - properties: any; -} - export interface IExperimentService { _serviceBrand: any; getExperimentById(id: string): TPromise; - getExperimentsToRunByType(type: ExperimentActionType): TPromise; + getExperimentsByType(type: ExperimentActionType): TPromise; getCuratedExtensionsList(curatedExtensionsKey: string): TPromise; markAsCompleted(experimentId: string): void; @@ -133,12 +136,12 @@ export class ExperimentService extends Disposable implements IExperimentService }); } - public getExperimentsToRunByType(type: ExperimentActionType): TPromise { + public getExperimentsByType(type: ExperimentActionType): TPromise { return this._loadExperimentsPromise.then(() => { if (type === ExperimentActionType.Custom) { - return this._experiments.filter(x => x.enabled && x.state === ExperimentState.Run && (!x.action || x.action.type === type)); + return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type)); } - return this._experiments.filter(x => x.enabled && x.state === ExperimentState.Run && x.action && x.action.type === type); + return this._experiments.filter(x => x.enabled && x.action && x.action.type === type); }); } @@ -252,18 +255,32 @@ export class ExperimentService extends Disposable implements IExperimentService this.storageService.store(storageKey, JSON.stringify(experimentState)); if (state === ExperimentState.Run) { - this._onExperimentEnabled.fire(processedExperiment); + this.fireRunExperiment(processedExperiment); } return TPromise.as(null); }); }); return TPromise.join(promises).then(() => { - this.telemetryService.publicLog('experiments', this._experiments); + /* __GDPR__ + "experiments" : { + "experiments" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('experiments', { experiments: this._experiments }); }); }); } + private fireRunExperiment(experiment: IExperiment) { + this._onExperimentEnabled.fire(experiment); + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + if (runExperimentIdsFromStorage.indexOf(experiment.id)) { + runExperimentIdsFromStorage.push(experiment.id); + this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(runExperimentIdsFromStorage)); + } + } + private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): TPromise { if (processedExperiment.state !== ExperimentState.Evaluating) { return TPromise.wrap(processedExperiment.state); @@ -277,6 +294,21 @@ export class ExperimentService extends Disposable implements IExperimentService return TPromise.wrap(ExperimentState.Run); } + if (experiment.condition.experimentsPreviouslyRun) { + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + let includeCheck = true; + let excludeCheck = true; + if (Array.isArray(experiment.condition.experimentsPreviouslyRun.includes)) { + includeCheck = runExperimentIdsFromStorage.some(x => experiment.condition.experimentsPreviouslyRun.includes.indexOf(x) > -1); + } + if (includeCheck && Array.isArray(experiment.condition.experimentsPreviouslyRun.excludes)) { + excludeCheck = !runExperimentIdsFromStorage.some(x => experiment.condition.experimentsPreviouslyRun.excludes.indexOf(x) > -1); + } + if (!includeCheck || !excludeCheck) { + return TPromise.wrap(ExperimentState.NoRun); + } + } + if (this.environmentService.appQuality === 'stable' && experiment.condition.insidersOnly === true) { return TPromise.wrap(ExperimentState.NoRun); } @@ -372,7 +404,7 @@ export class ExperimentService extends Disposable implements IExperimentService processedExperiment.state = latestExperimentState.state = Math.random() < experiment.condition.userProbability ? ExperimentState.Run : ExperimentState.NoRun; this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); if (latestExperimentState.state === ExperimentState.Run && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) { - this._onExperimentEnabled.fire(processedExperiment); + this.fireRunExperiment(processedExperiment); } } }); diff --git a/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts b/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts index fe37e429d49..31de30b8f56 100644 --- a/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts +++ b/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts @@ -621,12 +621,12 @@ suite('Experiment Service', () => { }; testObject = instantiationService.createInstance(TestExperimentService); - const custom = testObject.getExperimentsToRunByType(ExperimentActionType.Custom).then(result => { + const custom = testObject.getExperimentsByType(ExperimentActionType.Custom).then(result => { assert.equal(result.length, 2); assert.equal(result[0].id, 'simple-experiment'); assert.equal(result[1].id, 'custom-experiment'); }); - const prompt = testObject.getExperimentsToRunByType(ExperimentActionType.Prompt).then(result => { + const prompt = testObject.getExperimentsByType(ExperimentActionType.Prompt).then(result => { assert.equal(result.length, 2); assert.equal(result[0].id, 'prompt-with-no-commands'); assert.equal(result[1].id, 'prompt-with-commands'); @@ -634,7 +634,70 @@ suite('Experiment Service', () => { return TPromise.join([custom, prompt]); }); + test('experimentsPreviouslyRun includes, excludes check', () => { + experimentData = { + experiments: [ + { + id: 'experiment3', + enabled: true, + condition: { + experimentsPreviouslyRun: { + includes: ['experiment1'], + excludes: ['experiment2'] + } + } + }, + { + id: 'experiment4', + enabled: true, + condition: { + experimentsPreviouslyRun: { + includes: ['experiment1'], + excludes: ['experiment200'] + } + } + } + ] + }; + let storageDataExperiment3 = { enabled: true, state: ExperimentState.Evaluating }; + let storageDataExperiment4 = { enabled: true, state: ExperimentState.Evaluating }; + instantiationService.stub(IStorageService, { + get: (a, b, c) => { + switch (a) { + case 'currentOrPreviouslyRunExperiments': + return JSON.stringify(['experiment1', 'experiment2']); + default: + break; + } + return c; + }, + store: (a, b, c) => { + switch (a) { + case 'experiments.experiment3': + storageDataExperiment3 = JSON.parse(b); + break; + case 'experiments.experiment4': + storageDataExperiment4 = JSON.parse(b); + break; + default: + break; + } + } + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentsByType(ExperimentActionType.Custom).then(result => { + assert.equal(result.length, 2); + assert.equal(result[0].id, 'experiment3'); + assert.equal(result[0].state, ExperimentState.NoRun); + assert.equal(result[1].id, 'experiment4'); + assert.equal(result[1].state, ExperimentState.Run); + assert.equal(storageDataExperiment3.state, ExperimentState.NoRun); + assert.equal(storageDataExperiment4.state, ExperimentState.Run); + return TPromise.as(null); + }); + }); // test('Experiment with condition type FileEdit should increment editcount as appropriate', () => { // }); diff --git a/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts b/src/vs/workbench/parts/extensions/browser/extensionsViewer.ts similarity index 60% rename from src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts rename to src/vs/workbench/parts/extensions/browser/extensionsViewer.ts index 0e0c2715ee8..a2aede32774 100644 --- a/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsViewer.ts @@ -10,13 +10,15 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; import { Action } from 'vs/base/common/actions'; -import { IExtensionDependencies, IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; +import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/extensions/common/extensions'; import { once } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { WorkbenchTreeController, WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export interface IExtensionTemplateData { icon: HTMLImageElement; @@ -24,33 +26,36 @@ export interface IExtensionTemplateData { identifier: HTMLElement; author: HTMLElement; extensionDisposables: IDisposable[]; - extensionDependencies: IExtensionDependencies; + extensionData: IExtensionData; } export interface IUnknownExtensionTemplateData { identifier: HTMLElement; } +export interface IExtensionData { + extension: IExtension; + hasChildren: boolean; + getChildren: () => Promise; + parent: IExtensionData; +} + export class DataSource implements IDataSource { - public getId(tree: ITree, element: IExtensionDependencies): string { - let id = element.identifier; - this.getParent(tree, element).then(parent => { - id = parent ? this.getId(tree, parent) + '/' + id : id; - }); - return id; + public getId(tree: ITree, { extension, parent }: IExtensionData): string { + return parent ? this.getId(tree, parent) + '/' + extension.id : extension.id; } - public hasChildren(tree: ITree, element: IExtensionDependencies): boolean { - return element.hasDependencies; + public hasChildren(tree: ITree, { hasChildren }: IExtensionData): boolean { + return hasChildren; } - public getChildren(tree: ITree, element: IExtensionDependencies): Promise { - return TPromise.as(element.dependencies); + public getChildren(tree: ITree, extensionData: IExtensionData): Promise { + return extensionData.getChildren(); } - public getParent(tree: ITree, element: IExtensionDependencies): Promise { - return TPromise.as(element.dependent); + public getParent(tree: ITree, { parent }: IExtensionData): Promise { + return TPromise.as(parent); } } @@ -62,12 +67,12 @@ export class Renderer implements IRenderer { constructor(@IInstantiationService private instantiationService: IInstantiationService) { } - public getHeight(tree: ITree, element: IExtensionDependencies): number { + public getHeight(tree: ITree, element: IExtensionData): number { return 62; } - public getTemplateId(tree: ITree, element: IExtensionDependencies): string { - return element.extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; + public getTemplateId(tree: ITree, { extension }: IExtensionData): string { + return extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; } public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { @@ -78,7 +83,7 @@ export class Renderer implements IRenderer { } private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData { - dom.addClass(container, 'dependency'); + dom.addClass(container, 'extension'); const icon = dom.append(container, dom.$('img.icon')); const details = dom.append(container, dom.$('.details')); @@ -87,8 +92,8 @@ export class Renderer implements IRenderer { const name = dom.append(header, dom.$('span.name')); const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction); const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => { - tree.setFocus(openExtensionAction.extensionDependencies); - tree.setSelection([openExtensionAction.extensionDependencies]); + tree.setFocus(openExtensionAction.extensionData); + tree.setSelection([openExtensionAction.extensionData]); openExtensionAction.run(e.ctrlKey || e.metaKey); e.stopPropagation(); e.preventDefault(); @@ -103,22 +108,22 @@ export class Renderer implements IRenderer { identifier, author, extensionDisposables, - set extensionDependencies(e: IExtensionDependencies) { - openExtensionAction.extensionDependencies = e; + set extensionData(extensionData: IExtensionData) { + openExtensionAction.extensionData = extensionData; } }; } private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData { - const messageContainer = dom.append(container, dom.$('div.unknown-dependency')); + const messageContainer = dom.append(container, dom.$('div.unknown-extension')); dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error"); - dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Dependency', "Unknown Dependency:"); + dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:"); const identifier = dom.append(messageContainer, dom.$('span.message')); return { identifier }; } - public renderElement(tree: ITree, element: IExtensionDependencies, templateId: string, templateData: any): void { + public renderElement(tree: ITree, element: IExtensionData, templateId: string, templateData: any): void { if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { this.renderExtension(tree, element, templateData); return; @@ -126,9 +131,8 @@ export class Renderer implements IRenderer { this.renderUnknownExtension(tree, element, templateData); } - private renderExtension(tree: ITree, element: IExtensionDependencies, data: IExtensionTemplateData): void { - const extension = element.extension; - + private renderExtension(tree: ITree, extensionData: IExtensionData, data: IExtensionTemplateData): void { + const extension = extensionData.extension; const onError = once(domEvent(data.icon, 'error')); onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); data.icon.src = extension.iconUrl; @@ -143,11 +147,11 @@ export class Renderer implements IRenderer { data.name.textContent = extension.displayName; data.identifier.textContent = extension.id; data.author.textContent = extension.publisherDisplayName; - data.extensionDependencies = element; + data.extensionData = extensionData; } - private renderUnknownExtension(tree: ITree, element: IExtensionDependencies, data: IUnknownExtensionTemplateData): void { - data.identifier.textContent = element.identifier; + private renderUnknownExtension(tree: ITree, { extension }: IExtensionData, data: IUnknownExtensionTemplateData): void { + data.identifier.textContent = extension.id; } public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { @@ -169,10 +173,10 @@ export class Controller extends WorkbenchTreeController { this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true)); } - protected onLeftClick(tree: ITree, element: IExtensionDependencies, event: IMouseEvent): boolean { + protected onLeftClick(tree: ITree, element: IExtensionData, event: IMouseEvent): boolean { let currentFocused = tree.getFocus(); if (super.onLeftClick(tree, element, event)) { - if (element.dependent === null) { + if (element.parent === null) { if (currentFocused) { tree.setFocus(currentFocused); } else { @@ -185,7 +189,7 @@ export class Controller extends WorkbenchTreeController { } public openExtension(tree: ITree, sideByside: boolean): boolean { - const element: IExtensionDependencies = tree.getFocus(); + const element: IExtensionData = tree.getFocus(); if (element.extension) { this.extensionsWorkdbenchService.open(element.extension, sideByside); return true; @@ -196,21 +200,58 @@ export class Controller extends WorkbenchTreeController { class OpenExtensionAction extends Action { - private _extensionDependencies: IExtensionDependencies; + private _extensionData: IExtensionData; constructor(@IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService) { - super('extensions.action.openDependency', ''); + super('extensions.action.openExtension', ''); } - public set extensionDependencies(extensionDependencies: IExtensionDependencies) { - this._extensionDependencies = extensionDependencies; + public set extensionData(extension: IExtensionData) { + this._extensionData = extension; } - public get extensionDependencies(): IExtensionDependencies { - return this._extensionDependencies; + public get extensionData(): IExtensionData { + return this._extensionData; } run(sideByside: boolean): TPromise { - return this.extensionsWorkdbenchService.open(this._extensionDependencies.extension, sideByside); + return this.extensionsWorkdbenchService.open(this.extensionData.extension, sideByside); + } +} + +export class ExtensionsTree extends WorkbenchTree { + + constructor( + input: IExtensionData, + container: HTMLElement, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + const renderer = instantiationService.createInstance(Renderer); + const controller = instantiationService.createInstance(Controller); + + super( + container, + { + dataSource: new DataSource(), + renderer, + controller + }, { + indentPixels: 40, + twistiePixels: 20 + }, + contextKeyService, listService, themeService, instantiationService, configurationService + ); + + this.setInput(input); + + this.disposables.push(this.onDidChangeSelection(event => { + if (event && event.payload && event.payload.origin === 'keyboard') { + controller.openExtension(this, false); + } + })); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/common/extensionQuery.ts b/src/vs/workbench/parts/extensions/common/extensionQuery.ts index 8fb85a6d48e..4a3cc885ec5 100644 --- a/src/vs/workbench/parts/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/parts/extensions/common/extensionQuery.ts @@ -3,12 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { flatten } from 'vs/base/common/arrays'; + export class Query { constructor(public value: string, public sortBy: string, public groupBy: string) { this.value = value.trim(); } + static autocompletions(): string[] { + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'sort', 'category', 'tag', 'ext']; + const subcommands = { + 'sort': ['installs', 'rating', 'name'], + 'category': ['"programming languages"', 'snippets', 'linters', 'themes', 'debuggers', 'formatters', 'keymaps', '"scm providers"', 'other', '"extension packs"', '"language packs"'], + 'tag': [''], + 'ext': [''] + }; + + return flatten(commands.map(command => subcommands[command] ? subcommands[command].map(subcommand => `${command}:${subcommand}`) : [command])); + } + static parse(value: string): Query { let sortBy = ''; value = value.replace(/@sort:(\w+)(-\w*)?/g, (match, by: string, order: string) => { diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index 64b3a90a7b2..bf50d9d44ae 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -49,11 +49,14 @@ export interface IExtension { outdated: boolean; enablementState: EnablementState; dependencies: string[]; + extensionPack: string[]; telemetryData: any; preview: boolean; getManifest(): TPromise; getReadme(): TPromise; + hasReadme(): boolean; getChangelog(): TPromise; + hasChangelog(): boolean; local?: ILocalExtension; locals?: ILocalExtension[]; gallery?: IGalleryExtension; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts index f3a1da645e0..d7f89ad8915 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/extensionEditor'; import { localize } from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { marked } from 'vs/base/common/marked/marked'; import { always } from 'vs/base/common/async'; import * as arrays from 'vs/base/common/arrays'; @@ -27,7 +27,6 @@ import { IExtensionManifest, IKeyBinding, IView, IExtensionTipsService, LocalExt import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension, IExtensionDependencies } from 'vs/workbench/parts/extensions/common/extensions'; -import { Renderer, DataSource, Controller } from 'vs/workbench/parts/extensions/browser/dependenciesViewer'; import { RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -46,10 +45,11 @@ import { Command } from 'vs/editor/browser/editorExtensions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Color } from 'vs/base/common/color'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { assign } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ExtensionsTree, IExtensionData } from 'vs/workbench/parts/extensions/browser/extensionsViewer'; +import { ShowCurrentReleaseNotesAction } from 'vs/workbench/parts/update/electron-browser/update'; /** A context key that is set when an extension editor webview has focus. */ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS = new RawContextKey('extensionEditorWebviewFocus', undefined); @@ -138,7 +138,8 @@ const NavbarSection = { Readme: 'readme', Contributions: 'contributions', Changelog: 'changelog', - Dependencies: 'dependencies' + Dependencies: 'dependencies', + ExtensionPack: 'extensionPack' }; interface ILayoutParticipant { @@ -423,12 +424,27 @@ export class ExtensionEditor extends BaseEditor { this.navbar.clear(); this.navbar.onChange(this.onNavbarChange.bind(this, extension), this, this.transientDisposables); - this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); - this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); - this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); - this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); - this.editorLoadComplete = true; + if (extension.hasReadme()) { + this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); + } + this.extensionManifest.get() + .then(manifest => { + if (extension.extensionPack.length) { + this.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); + } + if (manifest.contributes) { + this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); + } + if (extension.hasChangelog()) { + this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); + } + if (extension.dependencies.length) { + this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); + } + this.editorLoadComplete = true; + }); + return super.setInput(input, options, token); } @@ -459,6 +475,7 @@ export class ExtensionEditor extends BaseEditor { case NavbarSection.Contributions: return this.openContributions(); case NavbarSection.Changelog: return this.openChangelog(); case NavbarSection.Dependencies: return this.openDependencies(extension); + case NavbarSection.ExtensionPack: return this.openExtensionPack(extension); } } @@ -477,8 +494,11 @@ export class ExtensionEditor extends BaseEditor { this.activeWebview.contents = body; this.activeWebview.onDidClickLink(link => { + if (!link) { + return; + } // Whitelist supported schemes for links - if (link && ['http', 'https', 'mailto'].indexOf(link.scheme) >= 0) { + if (['http', 'https', 'mailto'].indexOf(link.scheme) >= 0 || (link.scheme === 'command' && link.path === ShowCurrentReleaseNotesAction.ID)) { this.openerService.open(link); } }, null, this.contentDisposables); @@ -550,16 +570,16 @@ export class ExtensionEditor extends BaseEditor { append(this.content, scrollableContent.getDomNode()); this.contentDisposables.push(scrollableContent); - const tree = this.renderDependencies(content, extensionDependencies); + const dependenciesTree = this.renderDependencies(content, extensionDependencies); const layout = () => { scrollableContent.scanDomNode(); const scrollDimensions = scrollableContent.getScrollDimensions(); - tree.layout(scrollDimensions.height); + dependenciesTree.layout(scrollDimensions.height); }; const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); - this.contentDisposables.push(tree); + this.contentDisposables.push(dependenciesTree); scrollableContent.scanDomNode(); }, error => { append(this.content, $('p.nocontent')).textContent = error; @@ -569,32 +589,96 @@ export class ExtensionEditor extends BaseEditor { } private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): Tree { - const renderer = this.instantiationService.createInstance(Renderer); - const controller = this.instantiationService.createInstance(Controller); - const tree = this.instantiationService.createInstance(WorkbenchTree, container, { - dataSource: new DataSource(), - renderer, - controller - }, { - indentPixels: 40, - twistiePixels: 20 - }); + class ExtensionData implements IExtensionData { - tree.setInput(extensionDependencies); + private readonly extensionDependencies: IExtensionDependencies; - this.contentDisposables.push(tree.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - controller.openExtension(tree, false); + constructor(extensionDependencies: IExtensionDependencies) { + this.extensionDependencies = extensionDependencies; } - })); - return tree; + get extension(): IExtension { + return this.extensionDependencies.extension; + } + + get parent(): IExtensionData { + return this.extensionDependencies.dependent ? new ExtensionData(this.extensionDependencies.dependent) : null; + } + + get hasChildren(): boolean { + return this.extensionDependencies.hasDependencies; + } + + getChildren(): Promise { + return this.extensionDependencies.dependencies ? TPromise.as(this.extensionDependencies.dependencies.map(d => new ExtensionData(d))) : null; + } + } + + return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extensionDependencies), container); + } + + private openExtensionPack(extension: IExtension) { + return this.loadContents(() => { + const content = $('div', { class: 'subcontent' }); + const scrollableContent = new DomScrollableElement(content, {}); + append(this.content, scrollableContent.getDomNode()); + this.contentDisposables.push(scrollableContent); + + const dependenciesTree = this.renderExtensionPack(content, extension); + const layout = () => { + scrollableContent.scanDomNode(); + const scrollDimensions = scrollableContent.getScrollDimensions(); + dependenciesTree.layout(scrollDimensions.height); + }; + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); + this.contentDisposables.push(toDisposable(removeLayoutParticipant)); + + this.contentDisposables.push(dependenciesTree); + scrollableContent.scanDomNode(); + return TPromise.as(null); + }); + } + + private renderExtensionPack(container: HTMLElement, extension: IExtension): Tree { + const extensionsWorkbenchService = this.extensionsWorkbenchService; + class ExtensionData implements IExtensionData { + + readonly extension: IExtension; + readonly parent: IExtensionData; + + constructor(extension: IExtension, parent?: IExtensionData) { + this.extension = extension; + this.parent = parent; + } + + get hasChildren(): boolean { + return this.extension.extensionPack.length > 0; + } + + getChildren(): Promise { + if (this.hasChildren) { + const names = arrays.distinct(this.extension.extensionPack, e => e.toLowerCase()); + return extensionsWorkbenchService.queryGallery({ names, pageSize: names.length }) + .then(result => result.firstPage.map(extension => new ExtensionData(extension, this))); + } + return TPromise.as(null); + } + } + + return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension), container); } private renderSettings(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { const contributes = manifest.contributes; const configuration = contributes && contributes.configuration; - const properties = configuration && configuration.properties; + let properties = {}; + if (Array.isArray(configuration)) { + configuration.forEach(config => { + properties = { ...properties, ...config.properties }; + }); + } else if (configuration) { + properties = configuration.properties; + } const contrib = properties ? Object.keys(properties) : []; if (!contrib.length) { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 784c508d624..728d2b9e9b3 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -42,7 +42,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; import URI from 'vs/base/common/uri'; import { areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExperimentService, ExperimentActionType } from 'vs/workbench/parts/experiments/node/experimentService'; +import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { Schemas } from 'vs/base/common/network'; const milliSecondsInADay = 1000 * 60 * 60 * 24; const choiceNever = localize('neverShowAgain', "Don't Show Again"); @@ -81,11 +82,12 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private _workspaceIgnoredRecommendations: string[] = []; private _extensionsRecommendationsUrl: string; private _disposables: IDisposable[] = []; - public loadRecommendationsPromise: TPromise; + public loadWorkspaceConfigPromise: TPromise; private proactiveRecommendationsFetched: boolean = false; private readonly _onRecommendationChange: Emitter = new Emitter(); onRecommendationChange: Event = this._onRecommendationChange.event; + private sessionSeed: number; constructor( @IExtensionGalleryService private readonly _galleryService: IExtensionGalleryService, @@ -108,7 +110,6 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe ) { super(); - if (!this.isEnabled()) { return; } @@ -117,25 +118,24 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl; } + this.sessionSeed = +new Date(); + let globallyIgnored = JSON.parse(this.storageService.get('extensionsAssistant/ignored_recommendations', StorageScope.GLOBAL, '[]')); this._globallyIgnoredRecommendations = globallyIgnored.map(id => id.toLowerCase()); - this.loadRecommendationsPromise = this.getWorkspaceRecommendations() - .then(() => { - // these must be called after workspace configs have been refreshed. - this.fetchCachedDynamicWorkspaceRecommendations(); - this.fetchFileBasedRecommendations(); - this.fetchExperimentalRecommendations(); - return this.promptWorkspaceRecommendations(); - }).then(() => { - this._modelService.onModelAdded(this.promptFiletypeBasedRecommendations, this, this._disposables); - this._modelService.getModels().forEach(model => this.promptFiletypeBasedRecommendations(model)); - }); - + this.fetchCachedDynamicWorkspaceRecommendations(); + this.fetchFileBasedRecommendations(); + this.fetchExperimentalRecommendations(); if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { this.fetchProactiveRecommendations(true); } + this.loadWorkspaceConfigPromise = this.getWorkspaceRecommendations().then(() => { + this.promptWorkspaceRecommendations(); + this._modelService.onModelAdded(this.promptFiletypeBasedRecommendations, this, this._disposables); + this._modelService.getModels().forEach(model => this.promptFiletypeBasedRecommendations(model)); + }); + this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (!this.proactiveRecommendationsFetched && !this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { @@ -405,13 +405,13 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe ...this._dynamicWorkspaceRecommendations, ...Object.keys(this._experimentalRecommendations), ]).filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - shuffle(others); + shuffle(others, this.sessionSeed); return others.map(extensionId => { const sources: ExtensionRecommendationSource[] = []; if (this._exeBasedRecommendations[extensionId]) { sources.push('executable'); } - if (this._dynamicWorkspaceRecommendations[extensionId]) { + if (this._dynamicWorkspaceRecommendations.indexOf(extensionId) !== -1) { sources.push('dynamic'); } return ({ extensionId, sources }); @@ -500,7 +500,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe let hasSuggestion = false; const uri = model.uri; - if (!uri) { + if (!uri || uri.scheme !== Schemas.file) { return; } @@ -526,6 +526,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe recommendationsToSuggest.push(id); } const filedBasedRecommendation = this._fileBasedRecommendations[id.toLowerCase()] || { recommendedTime: now, sources: [] }; + filedBasedRecommendation.recommendedTime = now; if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { filedBasedRecommendation.sources.push(uri); } @@ -891,6 +892,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private fetchDynamicWorkspaceRecommendations(): TPromise { if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER + || this.contextService.getWorkspace().folders[0].uri.scheme !== Schemas.file // #54483: check with @Ramya || this._dynamicWorkspaceRecommendations.length || !this._extensionsRecommendationsUrl) { return TPromise.as(null); @@ -940,9 +942,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } private fetchExperimentalRecommendations() { - this.experimentService.getExperimentsToRunByType(ExperimentActionType.AddToRecommendations).then(experiments => { + this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations).then(experiments => { (experiments || []).forEach(experiment => { - if (experiment.action.properties && Array.isArray(experiment.action.properties.recommendations) && experiment.action.properties.recommendationReason) { + if (experiment.state === ExperimentState.Run && experiment.action.properties && Array.isArray(experiment.action.properties.recommendations) && experiment.action.properties.recommendationReason) { experiment.action.properties.recommendations.forEach(id => { this._experimentalRecommendations[id] = experiment.action.properties.recommendationReason; }); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index ea4c3874785..1415dd8bcfa 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import * as errors from 'vs/base/common/errors'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionGalleryService, IExtensionTipsService, ExtensionsLabel, ExtensionsChannelId, PreferencesLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService'; @@ -236,4 +236,26 @@ CommandsRegistry.registerCommand('_extensions.manage', (accessor: ServicesAccess if (extension.length === 1) { extensionService.open(extension[0]).done(null, errors.onUnexpectedError); } -}); \ No newline at end of file +}); + +// File menu registration + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '2_keybindings', + command: { + id: ShowRecommendedKeymapExtensionsAction.ID, + title: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions") + }, + order: 2 +}); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") + }, + order: 5 +}); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index 88964425ea0..ce4e5210290 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -2321,7 +2321,12 @@ export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigure } return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openExtensionsFile(configurationFile) + }]); }, err => { this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); }); @@ -2333,7 +2338,12 @@ export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigure } return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openExtensionsFile(configurationFile) + }]); }, err => { this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); }); @@ -2381,7 +2391,13 @@ export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecomm } return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openWorkspaceConfigurationFile(workspaceConfig) + }]); + }, err => { this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); }); @@ -2392,7 +2408,12 @@ export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecomm } return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId, shouldRecommend).then(() => { - this.notificationService.info(localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.')); + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openWorkspaceConfigurationFile(workspaceConfig) + }]); }, err => { this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); }); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts index 1f032611899..1153fd57fb8 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -11,7 +11,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Action } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDelegate } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { once } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; @@ -38,7 +38,7 @@ export interface ITemplateData { extensionDisposables: IDisposable[]; } -export class Delegate implements IDelegate { +export class Delegate implements IVirtualDelegate { getHeight() { return 62; } getTemplateId() { return 'extension'; } } @@ -194,6 +194,10 @@ export class Renderer implements IPagedRenderer { }); } + disposeElement(): void { + // noop + } + private updateRecommendationStatus(extension: IExtension, data: ITemplateData) { const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); let ariaLabel = extension.displayName + '. '; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 507bf50decd..0c222ed08c5 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -6,21 +6,21 @@ 'use strict'; import 'vs/css!./media/extensionsViewlet'; +import uri from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ThrottledDelayer, always } from 'vs/base/common/async'; import { TPromise } from 'vs/base/common/winjs.base'; import { isPromiseCanceledError, onUnexpectedError, create as createError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event as EventOf, mapEvent, chain } from 'vs/base/common/event'; +import { Event as EventOf, Emitter, chain } from 'vs/base/common/event'; import { IAction } from 'vs/base/common/actions'; -import { domEvent } from 'vs/base/browser/event'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { append, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom'; +import { append, $, addClass, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -39,7 +39,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorG import Severity from 'vs/base/common/severity'; import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { inputForeground, inputBackground, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { inputForeground, inputBackground, inputBorder, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewsRegistry, IViewDescriptor } from 'vs/workbench/common/views'; import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -58,6 +58,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SingleServerExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -252,12 +259,14 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private searchDelayer: ThrottledDelayer; private root: HTMLElement; - private searchBox: HTMLInputElement; + private searchBox: CodeEditorWidget; private extensionsBox: HTMLElement; private primaryActions: IAction[]; private secondaryActions: IAction[]; private groupByServerAction: IAction; private disposables: IDisposable[] = []; + private monacoStyleContainer: HTMLDivElement; + private placeholderText: HTMLDivElement; constructor( @IPartService partService: IPartService, @@ -275,7 +284,8 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, - @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService, + @IModelService private modelService: IModelService, ) { super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); @@ -299,6 +309,28 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); } }, this, this.disposables); + + modes.SuggestRegistry.register({ scheme: 'extensions', pattern: '**/searchinput', hasAccessToAllModels: true }, { + triggerCharacters: ['@'], + provideCompletionItems: (model: ITextModel, position: Position, _context: modes.SuggestContext) => { + const sortKey = (item: string) => { + if (item.indexOf(':') === -1) { return 'a'; } + else if (/ext:/.test(item) || /tag:/.test(item)) { return 'b'; } + else if (/sort:/.test(item)) { return 'c'; } + else { return 'd'; } + }; + return { + suggestions: this.autoComplete(model.getValue(), position.column).map(item => ( + { + label: item.fullText, + insertText: item.fullText, + overwriteBefore: item.overwrite, + sortText: sortKey(item.fullText), + type: 'keyword' + })) + }; + } + }); } create(parent: HTMLElement): TPromise { @@ -306,31 +338,36 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.root = parent; const header = append(this.root, $('.header')); - - this.searchBox = append(header, $('input.search-box')); - this.searchBox.placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); - this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.FOCUS, () => addClass(this.searchBox, 'synthetic-focus'))); - this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.BLUR, () => removeClass(this.searchBox, 'synthetic-focus'))); + this.monacoStyleContainer = append(header, $('.monaco-container')); + this.searchBox = this.instantiationService.createInstance(CodeEditorWidget, this.monacoStyleContainer, SEARCH_INPUT_OPTIONS, { isSimpleWidget: true }); + this.placeholderText = append(this.monacoStyleContainer, $('.search-placeholder', null, localize('searchExtensions', "Search Extensions in Marketplace"))); this.extensionsBox = append(this.root, $('.extensions')); - const onKeyDown = chain(domEvent(this.searchBox, 'keydown')) - .map(e => new StandardKeyboardEvent(e)); - onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables); + this.searchBox.setModel(this.modelService.createModel('', null, uri.parse('extensions:searchinput'), true)); - const onKeyDownForList = onKeyDown.filter(() => this.count() > 0); - onKeyDownForList.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables); + this.disposables.push(this.searchBox.onDidFocusEditorText(() => addClass(this.monacoStyleContainer, 'synthetic-focus'))); + this.disposables.push(this.searchBox.onDidBlurEditorText(() => removeClass(this.monacoStyleContainer, 'synthetic-focus'))); - const onSearchInput = domEvent(this.searchBox, 'input') as EventOf; - onSearchInput(e => this.triggerSearch(e.immediate), null, this.disposables); + const onKeyDownMonaco = chain(this.searchBox.onKeyDown); + onKeyDownMonaco.filter(e => e.keyCode === KeyCode.Enter).on(e => e.preventDefault(), this, this.disposables); + onKeyDownMonaco.filter(e => e.keyCode === KeyCode.DownArrow).on(() => this.focusListView(), this, this.disposables); - this.onSearchChange = mapEvent(onSearchInput, e => e.target.value); + const searchChangeEvent = new Emitter(); + this.onSearchChange = searchChangeEvent.event; + + this.disposables.push(this.searchBox.getModel().onDidChangeContent(() => { + this.triggerSearch(); + const content = this.searchBox.getValue(); + searchChangeEvent.fire(content); + this.placeholderText.style.visibility = content ? 'hidden' : 'visible'; + })); return super.create(this.extensionsBox) .then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User)) .then(installed => { if (installed.length === 0) { - this.searchBox.value = '@sort:installs'; + this.searchBox.setValue('@sort:installs'); this.searchExtensionsContextKey.set(true); } }); @@ -339,13 +376,19 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio public updateStyles(): void { super.updateStyles(); - this.searchBox.style.backgroundColor = this.getColor(inputBackground); - this.searchBox.style.color = this.getColor(inputForeground); + this.monacoStyleContainer.style.backgroundColor = this.getColor(inputBackground); + this.monacoStyleContainer.style.color = this.getColor(inputForeground); + this.placeholderText.style.color = this.getColor(inputPlaceholderForeground); const inputBorderColor = this.getColor(inputBorder); - this.searchBox.style.borderWidth = inputBorderColor ? '1px' : null; - this.searchBox.style.borderStyle = inputBorderColor ? 'solid' : null; - this.searchBox.style.borderColor = inputBorderColor; + this.monacoStyleContainer.style.borderWidth = inputBorderColor ? '1px' : null; + this.monacoStyleContainer.style.borderStyle = inputBorderColor ? 'solid' : null; + this.monacoStyleContainer.style.borderColor = inputBorderColor; + + let cursor = this.monacoStyleContainer.getElementsByClassName('cursor')[0] as HTMLDivElement; + if (cursor) { + cursor.style.backgroundColor = this.getColor(inputForeground); + } } setVisible(visible: boolean): TPromise { @@ -354,7 +397,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio if (isVisibilityChanged) { if (visible) { this.searchBox.focus(); - this.searchBox.setSelectionRange(0, this.searchBox.value.length); + this.searchBox.setSelection(new Range(1, 1, 1, this.searchBox.getValue().length + 1)); } } }); @@ -366,6 +409,9 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio layout(dimension: Dimension): void { toggleClass(this.root, 'narrow', dimension.width <= 300); + this.searchBox.layout({ height: 20, width: dimension.width - 30 }); + this.placeholderText.style.width = '' + (dimension.width - 30) + 'px'; + super.layout(new Dimension(dimension.width, dimension.height - 38)); } @@ -420,17 +466,19 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio const event = new Event('input', { bubbles: true }) as SearchInputEvent; event.immediate = true; - this.searchBox.value = value; - this.searchBox.dispatchEvent(event); + this.searchBox.setValue(value); } private triggerSearch(immediate = false): void { - this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.value ? 0 : 500) - .done(null, err => this.onError(err)); + this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.getValue() ? 0 : 500).done(null, err => this.onError(err)); + } + + private normalizedQuery(): string { + return (this.searchBox.getValue() || '').replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:'); } private doSearch(): TPromise { - const value = this.searchBox.value || ''; + const value = this.normalizedQuery(); this.searchExtensionsContextKey.set(!!value); this.searchInstalledExtensionsContextKey.set(InstalledExtensionsView.isInstalledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); @@ -439,14 +487,14 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); if (value) { - return 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.normalizedQuery())))); } return TPromise.as(null); } protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { const addedViews = super.onDidAddViews(added); - this.progress(TPromise.join(addedViews.map(addedView => (addedView).show(this.searchBox.value)))); + this.progress(TPromise.join(addedViews.map(addedView => (addedView).show(this.normalizedQuery())))); return addedViews; } @@ -464,16 +512,23 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio return this.instantiationService.createInstance(viewDescriptor.ctor, options) as ViewletPanel; } + private autoComplete(query: string, position: number): { fullText: string, overwrite: number }[] { + if (query.lastIndexOf('@', position - 1) !== query.lastIndexOf(' ', position - 1) + 1) { return []; } + + let wordStart = query.lastIndexOf('@', position - 1) + 1; + let alreadyTypedCount = position - wordStart - 1; + + return Query.autocompletions().map(replacement => ({ fullText: replacement, overwrite: alreadyTypedCount })); + } + private count(): number { return this.panels.reduce((count, view) => (view).count() + count, 0); } - private onEscape(): void { - this.search(''); - } - - private onEnter(): void { - (this.panels[0]).select(); + private focusListView(): void { + if (this.count() > 0) { + this.panels[0].focus(); + } } private onViewletOpen(viewlet: IViewlet): void { @@ -607,4 +662,34 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} + +let SEARCH_INPUT_OPTIONS: IEditorOptions = +{ + fontSize: 13, + lineHeight: 22, + wordWrap: 'off', + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + selectOnLineNumbers: false, + hideCursorInOverviewRuler: true, + selectionHighlight: false, + scrollbar: { + horizontal: 'hidden', + vertical: 'hidden' + }, + ariaLabel: localize('searchExtensions', "Search Extensions in Marketplace"), + cursorWidth: 1, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + acceptSuggestionOnEnter: 'smart', + minimap: { + enabled: false + }, + fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif' +}; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index a03d325d251..02cd5b25c9f 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -71,9 +71,8 @@ export class ExtensionsListView extends ViewletPanel { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); } - renderHeader(container: HTMLElement): void { - const titleDiv = append(container, $('div.title')); - append(titleDiv, $('span')).textContent = this.options.title; + renderHeaderTitle(container: HTMLElement): void { + super.renderHeaderTitle(container, this.options.title); this.badgeContainer = append(container, $('.count-badge-wrapper')); this.badge = new CountBadge(this.badgeContainer); @@ -187,8 +186,8 @@ export class ExtensionsListView extends ViewletPanel { const basics = result.filter(e => { return e.local.manifest && e.local.manifest.contributes - && Array.isArray(e.local.manifest.contributes.languages) - && e.local.manifest.contributes.languages.length + && Array.isArray(e.local.manifest.contributes.grammars) + && e.local.manifest.contributes.grammars.length && e.local.identifier.id !== 'git'; }); return new PagedModel(this.sortExtensions(basics, options)); @@ -197,7 +196,7 @@ export class ExtensionsListView extends ViewletPanel { const others = result.filter(e => { return e.local.manifest && e.local.manifest.contributes - && (!Array.isArray(e.local.manifest.contributes.languages) || e.local.identifier.id === 'git') + && (!Array.isArray(e.local.manifest.contributes.grammars) || e.local.identifier.id === 'git') && !Array.isArray(e.local.manifest.contributes.themes); }); return new PagedModel(this.sortExtensions(others, options)); @@ -641,6 +640,14 @@ export class ExtensionsListView extends ViewletPanel { static isKeymapsRecommendedExtensionsQuery(query: string): boolean { return /@recommended:keymaps/i.test(query); } + + focus(): void { + super.focus(); + if (!(this.list.getFocus().length || this.list.getSelection().length)) { + this.list.focusNext(); + } + this.list.domFocus(); + } } export class InstalledExtensionsView extends ExtensionsListView { diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css index 10efc9daede..7a8a61811e5 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css @@ -311,11 +311,11 @@ border-color: rgb(238, 238, 238); } -.extension-editor .subcontent .monaco-tree-row .content .unknown-dependency { +.extension-editor .subcontent .monaco-tree-row .content .unknown-extension { line-height: 62px; } -.extension-editor .subcontent .monaco-tree-row .content .unknown-dependency > .error-marker { +.extension-editor .subcontent .monaco-tree-row .content .unknown-extension > .error-marker { background-color: #BE1100; padding: 2px 4px; font-weight: bold; @@ -323,46 +323,46 @@ color: #CCC; } -.extension-editor .subcontent .monaco-tree-row .unknown-dependency > .message { +.extension-editor .subcontent .monaco-tree-row .unknown-extension > .message { padding-left: 10px; font-weight: bold; font-size: 14px; } -.extension-editor .subcontent .monaco-tree-row .dependency { +.extension-editor .subcontent .monaco-tree-row .extension { display: flex; align-items: center; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details { +.extension-editor .subcontent .monaco-tree-row .extension > .details { flex: 1; overflow: hidden; padding-left: 10px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header { display: flex; align-items: center; height: 19px; overflow: hidden; } -.extension-editor .subcontent .monaco-tree-row .dependency > .icon { +.extension-editor .subcontent .monaco-tree-row .extension > .icon { height: 40px; width: 40px; object-fit: contain; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .name { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name { font-weight: bold; font-size: 16px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .name:hover { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name:hover { text-decoration: underline; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .identifier { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .identifier { font-size: 90%; opacity: 0.6; margin-left: 10px; @@ -371,14 +371,14 @@ border-radius: 4px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .footer { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer { display: flex; height: 19px; overflow: hidden; padding-top: 5px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .footer > .author { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer > .author { font-size: 90%; font-weight: 600; opacity: 0.6; diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css index 848c0382f9a..a879a47e71a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -6,4 +6,9 @@ .monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.extensions { -webkit-mask: url('extensions-dark.svg') no-repeat 50% 50%; -webkit-mask-size: 21px; +} + +.extensions .split-view-view .panel-header .count-badge-wrapper { + position: absolute; + right: 12px; } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css index 3d7ffd21842..a39459a99a1 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css @@ -202,6 +202,32 @@ opacity: 0.9; } +.extensions-viewlet .header .monaco-container { + padding: 3px 4px 5px; +} + +.extensions-viewlet .header .monaco-container .suggest-widget { + width: 275px; +} + +.extensions-viewlet .header .monaco-container .monaco-editor-background, +.extensions-viewlet .header .monaco-container .monaco-editor, +.extensions-viewlet .header .monaco-container .mtk1 { + /* allow the embedded monaco to be styled from the outer context */ + background-color: inherit; + color: inherit; +} + +.extensions-viewlet .header .search-placeholder { + position: absolute; + z-index: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + margin-top: 2px; +} + .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, .vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon, diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.png b/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.png deleted file mode 100644 index 7f7e56c098e..00000000000 Binary files a/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.png and /dev/null differ diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.svg b/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.svg new file mode 100755 index 00000000000..de095424278 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.svg @@ -0,0 +1 @@ +Market_LanguageGeneric \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index ed9c80d3b1a..0d1d08f98b9 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -21,7 +21,7 @@ import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/exte import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService, IExtensionDescription, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -216,7 +216,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { const TEMPLATE_ID = 'runtimeExtensionElementTemplate'; - const delegate = new class implements IDelegate{ + const delegate = new class implements IVirtualDelegate{ getHeight(element: IRuntimeExtension): number { return 62; } @@ -369,6 +369,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { } }, + disposeElement: () => null, + disposeTemplate: (data: IRuntimeExtensionTemplateData): void => { data.disposables = dispose(data.disposables); } diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 03a9cc7540c..a492f570789 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -150,8 +150,8 @@ class Extension implements IExtension { if (Array.isArray(this.local.manifest.contributes.themes) && this.local.manifest.contributes.themes.length) { return require.toUrl('../electron-browser/media/theme-icon.png'); } - if (Array.isArray(this.local.manifest.contributes.languages) && this.local.manifest.contributes.languages.length) { - return require.toUrl('../electron-browser/media/language-icon.png'); + if (Array.isArray(this.local.manifest.contributes.grammars) && this.local.manifest.contributes.grammars.length) { + return require.toUrl('../electron-browser/media/language-icon.svg'); } } } @@ -218,6 +218,18 @@ class Extension implements IExtension { return TPromise.as(this.local.manifest); } + hasReadme(): boolean { + if (this.gallery && !this.isGalleryOutdated() && this.gallery.assets.readme) { + return true; + } + + if (this.local && this.local.readmeUrl) { + return true; + } + + return this.type === LocalExtensionType.System; + } + getReadme(): TPromise { if (this.gallery && !this.isGalleryOutdated()) { if (this.gallery.assets.readme) { @@ -233,7 +245,8 @@ class Extension implements IExtension { if (this.type === LocalExtensionType.System) { return TPromise.as(`# ${this.displayName || this.name} -**Notice** This is a an extension that is bundled with Visual Studio Code. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. +## Features ${this.description} `); } @@ -241,6 +254,19 @@ ${this.description} return TPromise.wrapError(new Error('not available')); } + hasChangelog(): boolean { + if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) { + return true; + } + + if (this.local && this.local.changelogUrl) { + const uri = URI.parse(this.local.changelogUrl); + return uri.scheme === 'file'; + } + + return this.type === LocalExtensionType.System; + } + getChangelog(): TPromise { if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) { return this.galleryService.getChangelog(this.gallery); @@ -249,6 +275,10 @@ ${this.description} const changelogUrl = this.local && this.local.changelogUrl; if (!changelogUrl) { + if (this.type === LocalExtensionType.System) { + return TPromise.as('Please check the [VS Code Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.'); + } + return TPromise.wrapError(new Error('not available')); } @@ -264,13 +294,24 @@ ${this.description} get dependencies(): string[] { const { local, gallery } = this; if (gallery && !this.isGalleryOutdated()) { - return gallery.properties.dependencies; + return gallery.properties.dependencies || []; } if (local && local.manifest.extensionDependencies) { return local.manifest.extensionDependencies; } return []; } + + get extensionPack(): string[] { + const { local, gallery } = this; + if (gallery && !this.isGalleryOutdated()) { + return gallery.properties.extensionPack || []; + } + if (local && local.manifest.extensionPack) { + return local.manifest.extensionPack; + } + return []; + } } class ExtensionDependencies implements IExtensionDependencies { @@ -302,7 +343,7 @@ class ExtensionDependencies implements IExtensionDependencies { if (!this.hasDependencies) { return []; } - return this._extension.dependencies.map(d => new ExtensionDependencies(this._map.get(d), d, this._map, this)); + return this._extension.dependencies.map(id => new ExtensionDependencies(this._map.get(id), id, this._map, this)); } private computeHasDependencies(): boolean { @@ -438,7 +479,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, .then(([allRecommendations, report]) => { const maliciousSet = getMaliciousExtensionsSet(report); - return this.galleryService.loadAllDependencies((extension).dependencies.map(id => { id })) + return this.galleryService.loadAllDependencies((extension).dependencies.map(id => ({ id }))) .then(galleryExtensions => galleryExtensions.map(galleryExtension => this.fromGallery(galleryExtension, maliciousSet, allRecommendations))) .then(extensions => [...this.local, ...extensions]) .then(extensions => { @@ -654,7 +695,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, location: ProgressLocation.Extensions, title: nls.localize('installingMarketPlaceExtension', 'Installing extension from Marketplace....'), source: `${extension.id}` - }, () => this.extensionService.installFromGallery(gallery).then(() => null)); + }, () => this.extensionService.installFromGallery(gallery)); } setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): TPromise { @@ -701,46 +742,31 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): TPromise { - const allDependencies = this.getDependenciesRecursively(extensions, this.local, enablementState, []); - if (allDependencies.length > 0) { - if (enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled) { - return this.promptForDependenciesAndEnable(extensions, allDependencies, enablementState); + const allDependenciesAndPackedExtensions = this.getDependenciesAndPackedExtensionsRecursively(extensions, this.local, enablementState); + if (allDependenciesAndPackedExtensions.length > 0) { + if (extensions.length === 1 && (enablementState === EnablementState.Disabled || enablementState === EnablementState.WorkspaceDisabled)) { + return this.promptForDependenciesAndDisable(extensions[0], allDependenciesAndPackedExtensions, enablementState); } else { - return this.promptForDependenciesAndDisable(extensions, allDependencies, enablementState); + return this.checkAndSetEnablement(extensions, allDependenciesAndPackedExtensions, enablementState); } } return this.checkAndSetEnablement(extensions, [], enablementState); } - private promptForDependenciesAndEnable(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const message = nls.localize('enableDependeciesConfirmation', "Enabling an extension also enables its dependencies. Would you like to continue?"); + private promptForDependenciesAndDisable(extension: IExtension, dependencies: IExtension[], enablementState: EnablementState): TPromise { + const message = nls.localize('disableExtensionPackConfirmation', "Would you like to disable '{0}' only or as a pack?", extension.displayName); const buttons = [ - nls.localize('enable', "Yes"), - nls.localize('doNotEnable', "No") - ]; - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 1 }) - .then(value => { - if (value === 0) { - return this.checkAndSetEnablement(extensions, dependencies, enablementState); - } - return TPromise.as(null); - }); - } - - private promptForDependenciesAndDisable(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const message = nls.localize('disableDependeciesConfirmation', "Would you like to disable the dependencies of the extensions also?"); - const buttons = [ - nls.localize('yes', "Yes"), - nls.localize('no', "No"), + nls.localize('disablePack', "Disable Extension Pack"), + nls.localize('disableOnly', "Disable Extension Only"), nls.localize('cancel', "Cancel") ]; - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 }) + return this.dialogService.show(Severity.Info, message, buttons) .then(value => { if (value === 0) { - return this.checkAndSetEnablement(extensions, dependencies, enablementState); + return this.checkAndSetEnablement([extension], dependencies, enablementState); } if (value === 1) { - return this.checkAndSetEnablement(extensions, [], enablementState); + return this.checkAndSetEnablement([extension], [], enablementState); } return TPromise.as(null); }); @@ -760,25 +786,26 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return TPromise.join(allExtensions.map(e => this.doSetEnablement(e, enablementState))); } - private getDependenciesRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, checked: IExtension[]): IExtension[] { + private getDependenciesAndPackedExtensionsRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, checked: IExtension[] = []): IExtension[] { const toCheck = extensions.filter(e => checked.indexOf(e) === -1); if (toCheck.length) { for (const extension of toCheck) { checked.push(extension); } - const dependenciesToDisable = installed.filter(i => { + const extensionsToDisable = installed.filter(i => { if (checked.indexOf(i) !== -1) { return false; } if (i.enablementState === enablementState) { return false; } - return i.type === LocalExtensionType.User && extensions.some(extension => extension.dependencies.indexOf(i.id) !== -1); + return i.type === LocalExtensionType.User && + extensions.some(extension => extension.dependencies.some(id => areSameExtensions({ id }, i)) || extension.extensionPack.some(id => areSameExtensions({ id }, i))); }); - if (dependenciesToDisable.length) { - const depsOfDeps = this.getDependenciesRecursively(dependenciesToDisable, installed, enablementState, checked); - return [...dependenciesToDisable, ...depsOfDeps]; + if (extensionsToDisable.length) { + extensionsToDisable.push(...this.getDependenciesAndPackedExtensionsRecursively(extensionsToDisable, installed, enablementState, checked)); } + return extensionsToDisable; } return []; } @@ -797,12 +824,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, if (extensionsToDisable.indexOf(i) !== -1) { return false; } - return i.dependencies.some(dep => { - if (extension.id === dep) { - return true; - } - return extensionsToDisable.some(d => d.id === dep); - }); + return i.dependencies.some(dep => [extension, ...extensionsToDisable].some(d => d.id === dep)); }); } 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 926f041931e..1c34aeeffa7 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 @@ -16,7 +16,7 @@ import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, LocalExtensionType, IGalleryExtension, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionId, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService, getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/common/extensionEnablementService.test'; @@ -79,12 +79,12 @@ suite('ExtensionsActions Test', () => { instantiationService.stub(IURLService, URLService); }); - setup(() => { + setup(async () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stub(IExtensionService, { getExtensions: () => TPromise.wrap([]) }); - (instantiationService.get(IExtensionEnablementService)).reset(); + await (instantiationService.get(IExtensionEnablementService)).reset(); instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); }); @@ -1207,6 +1207,7 @@ suite('ExtensionsActions Test', () => { assign(localExtension.manifest, { name, publisher: 'pub', version: '1.0.0' }, manifest); localExtension.identifier = { id: getLocalExtensionIdFromManifest(localExtension.manifest) }; localExtension.metadata = { id: localExtension.identifier.id, publisherId: localExtension.manifest.publisher, publisherDisplayName: 'somename' }; + localExtension.galleryIdentifier = { id: getGalleryExtensionIdFromLocal(localExtension), uuid: void 0 }; return localExtension; } diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts index faa0bf3f29f..89cbd28c3d6 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -287,7 +287,7 @@ suite('ExtensionsTipsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); }); @@ -297,7 +297,7 @@ suite('ExtensionsTipsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - assert.equal(!testObject.loadRecommendationsPromise, true); + assert.equal(!testObject.loadWorkspaceConfigPromise, true); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -327,7 +327,7 @@ suite('ExtensionsTipsService Test', () => { test('ExtensionTipsService: Prompt for valid workspace recommendations', () => { return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); @@ -359,7 +359,7 @@ suite('ExtensionsTipsService Test', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, 0); assert.ok(!prompted); }); @@ -387,7 +387,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been globally ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -407,7 +407,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been workspace ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -435,7 +435,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); @@ -462,7 +462,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); assert.ok(recommendations['mockpublisher1.mockextension1']); @@ -516,7 +516,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-vscode.csharp')); // stored recommendation that exists in product.extensionTips @@ -535,7 +535,7 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.loadRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-vscode.csharp')); // stored recommendation that exists in product.extensionTips diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 2b3f43d0114..704d31d312a 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -17,7 +17,7 @@ import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, LocalExtensionType, IGalleryExtension, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, EnablementState, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionId, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService, getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/common/extensionEnablementService.test'; @@ -40,7 +40,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { URLService } from 'vs/platform/url/common/urlService'; import URI from 'vs/base/common/uri'; -suite('ExtensionsWorkbenchService Test', () => { +suite('ExtensionsWorkbenchServiceTest', () => { let instantiationService: TestInstantiationService; let testObject: IExtensionsWorkbenchService; @@ -82,13 +82,13 @@ suite('ExtensionsWorkbenchService Test', () => { instantiationService.stub(IDialogService, { show: () => TPromise.as(0) }); }); - setup(() => { + setup(async () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stub(IDialogService, { show: () => TPromise.as(0) }); instantiationService.stubPromise(INotificationService, 'prompt', 0); - (instantiationService.get(IExtensionEnablementService)).reset(); + await (instantiationService.get(IExtensionEnablementService)).reset(); }); teardown(() => { @@ -851,6 +851,27 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension pack disable only itself', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + instantiationService.stubPromise(IDialogService, 'show', 1); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); + assert.equal(testObject.local[1].enablementState, EnablementState.Enabled); + }); + }); + }); + test('test disable extension with dependencies disable all', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -872,6 +893,27 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension pack disable all', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + instantiationService.stubPromise(IDialogService, 'show', 0); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); + assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); + }); + }); + }); + test('test disable extension fails if extension is a dependent of other', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -887,6 +929,24 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension when extension is part of a pack', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + return testObject.setEnablement(testObject.local[1], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); + }); + }); + }); + test('test disable both dependency and dependent do not promot and do not fail', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -1076,27 +1136,6 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); - test('test enable extension with dependencies does not enable if cancelled', () => { - const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); - const extensionB = aLocalExtension('b'); - const extensionC = aLocalExtension('c'); - - return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Disabled) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Disabled)) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Disabled)) - .then(() => { - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 1); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); - - return testObject.setEnablement(testObject.local[0], EnablementState.Enabled) - .then(() => { - assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); - }); - }); - }); - test('test enable extension with dependencies does not prompt if dependency is enabled already', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -1199,6 +1238,7 @@ suite('ExtensionsWorkbenchService Test', () => { assign(localExtension.manifest, { name, publisher: 'pub', version: '1.0.0' }, manifest); localExtension.identifier = { id: getLocalExtensionIdFromManifest(localExtension.manifest) }; localExtension.metadata = { id: localExtension.identifier.id, publisherId: localExtension.manifest.publisher, publisherDisplayName: 'somename' }; + localExtension.galleryIdentifier = { id: getGalleryExtensionIdFromLocal(localExtension), uuid: void 0 }; return localExtension; } diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 29b277207b2..8ec1880198a 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -15,7 +15,7 @@ import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditor import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference } from 'vs/base/common/lifecycle'; @@ -260,7 +260,8 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { 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 + allowBinary: this.forceOpenAsText, + reason: LoadReason.EDITOR }).then(model => { // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index bdb52e71e26..92490a50246 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -279,19 +279,6 @@ export class ExplorerItem { return this.children.size; } - public getChildrenNames(): string[] { - if (!this.children) { - return []; - } - - const names: string[] = []; - this.children.forEach(child => { - names.push(child.name); - }); - - return names; - } - /** * Removes a child element from this folder. */ diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index 9f88aa03a21..36e761a3e9b 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -476,5 +476,71 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { }); // Empty Editor Group Context Menu -MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.files.newUntitledFile', title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: GlobalNewUntitledFileAction.ID, title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 }); + +// File menu + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '1_new', + command: { + id: GlobalNewUntitledFileAction.ID, + title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: SAVE_FILE_COMMAND_ID, + title: nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: SAVE_FILE_AS_COMMAND_ID, + title: nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: SaveAllAction.ID, + title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '5_autosave', + command: { + id: ToggleAutoSaveAction.ID, + title: nls.localize('miAutoSave', "Auto Save") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: REVERT_FILE_COMMAND_ID, + title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File"), + precondition: DirtyEditorContext + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CLOSE_EDITOR_COMMAND_ID, + title: nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "&&Close Editor") + }, + order: 2 +}); diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 75e5b8c2d8c..04b6594db4e 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -18,7 +18,6 @@ import { posix } from 'path'; import * as errors from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as strings from 'vs/base/common/strings'; -import * as diagnostics from 'vs/base/common/diagnostics'; import { Action, IAction } from 'vs/base/common/actions'; import { MessageType, IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITree, IHighlightEvent } from 'vs/base/parts/tree/browser/tree'; @@ -1444,7 +1443,7 @@ export class ShowOpenedFileInNewWindow extends Action { public run(): TPromise { const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: true, filter: Schemas.file /* todo@remote */ }); if (fileResource) { - this.windowService.openWindow([fileResource.fsPath], { forceNewWindow: true, forceOpenWorkspaceAsFile: true }); + this.windowService.openWindow([fileResource], { forceNewWindow: true, forceOpenWorkspaceAsFile: true }); } else { this.notificationService.info(nls.localize('openFileToShowInNewWindow', "Open a file first to open in new window")); } @@ -1580,14 +1579,6 @@ class ClipboardContentProvider implements ITextModelContentProvider { } } -// Diagnostics support -let diag: (...args: any[]) => void; -if (!diag) { - diag = diagnostics.register('FileActionsDiagnostics', function (...args: any[]) { - console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])'); - }); -} - interface IExplorerContext { viewletState: IFileViewletState; stat: ExplorerItem; diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index 013fab15004..1bbf1bcaec2 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -79,10 +79,10 @@ export const ResourceSelectedForCompareContext = new RawContextKey('res export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); -export const openWindowCommand = (accessor: ServicesAccessor, paths: string[], forceNewWindow: boolean) => { +//TODO #54483 support string paths for backward compatibility. check with @bpasero and remove if not necessary +export const openWindowCommand = (accessor: ServicesAccessor, paths: (string | URI)[], forceNewWindow: boolean) => { const windowService = accessor.get(IWindowService); - - windowService.openWindow(paths, { forceNewWindow }); + windowService.openWindow(paths.map(p => typeof p === 'string' ? URI.file(p) : p), { forceNewWindow }); }; function save( @@ -436,6 +436,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), when: EditorContextKeys.focus.toNegated(), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C, + win: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C) + }, id: COPY_RELATIVE_PATH_COMMAND_ID, handler: (accessor, resource: URI | object) => { const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index dfe8322c5ae..c684a9a1e73 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ToggleViewletAction } from 'vs/workbench/browser/viewlet'; import * as nls from 'vs/nls'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; @@ -192,7 +192,7 @@ configurationRegistry.registerConfiguration({ }, 'files.associations': { 'type': 'object', - 'description': nls.localize('associations', "Configure file associations to languages (e.g. \"*.extension\": \"html\"). These have precedence over the default associations of the languages installed."), + 'description': nls.localize('associations', "Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed."), }, 'files.encoding': { 'type': 'string', @@ -246,17 +246,17 @@ configurationRegistry.registerConfiguration({ 'enum': [AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE], 'enumDescriptions': [ nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.off' }, "A dirty file is never automatically saved."), - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "A dirty file is automatically saved after the configured 'files.autoSaveDelay'."), + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "A dirty file is automatically saved after the configured `#files.autoSaveDelay#`."), nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty file is automatically saved when the editor loses focus."), nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty file is automatically saved when the window loses focus.") ], 'default': AutoSaveConfiguration.OFF, - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Accepted values: '{0}', '{1}', '{2}' (editor loses focus), '{3}' (window loses focus). If set to '{4}', you can configure the delay in 'files.autoSaveDelay'.", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) + 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) }, 'files.autoSaveDelay': { 'type': 'number', 'default': 1000, - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when 'files.autoSave' is set to '{0}'", AutoSaveConfiguration.AFTER_DELAY) + 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY) }, 'files.watcherExclude': { 'type': 'object', @@ -308,7 +308,7 @@ configurationRegistry.registerConfiguration({ 'editor.formatOnSaveTimeout': { 'type': 'number', 'default': 750, - 'description': nls.localize('formatOnSaveTimeout', "Format on save timeout. Specifies a time limit in milliseconds for formatOnSave-commands. Commands taking longer than the specified timeout will be cancelled."), + 'description': nls.localize('formatOnSaveTimeout', "Format on save timeout. Specifies a time limit in milliseconds for `formatOnSave`-commands. Commands taking longer than the specified timeout will be cancelled."), 'overridable': true, 'scope': ConfigurationScope.RESOURCE } @@ -371,3 +371,13 @@ configurationRegistry.registerConfiguration({ }, } }); + +// View menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css index eb657380a67..c3fc2504f70 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -59,7 +59,7 @@ } .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row:hover > .monaco-action-bar, -.explorer-viewlet .explorer-open-editors .monaco-list.focused .monaco-list-row.focused > .monaco-action-bar, +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.focused > .monaco-action-bar, .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty > .monaco-action-bar { visibility: visible; } 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 09615b2bae6..a59212c44df 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -275,7 +275,11 @@ export class FileRenderer implements IRenderer { const extraClasses = ['explorer-item', 'explorer-item-edited']; const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : (stat.isDirectory || (stat instanceof NewStatPlaceholder && stat.isDirectoryPlaceholder())) ? FileKind.FOLDER : FileKind.FILE; const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses }; - label.setFile(stat.resource, labelOptions); + + const parent = stat.name ? resources.dirname(stat.resource) : stat.resource; + const value = stat.name || ''; + + label.setFile(parent.with({ path: paths.join(parent.path, value || ' ') }), labelOptions); // Use icon for ' ' if name is empty. // Input field for name const inputBox = new InputBox(label.element, this.contextViewService, { @@ -286,12 +290,10 @@ export class FileRenderer implements IRenderer { }); const styler = attachInputBoxStyler(inputBox, this.themeService); - const parent = resources.dirname(stat.resource); inputBox.onDidChange(value => { - label.setFile(parent.with({ path: paths.join(parent.path, value) }), labelOptions); // update label icon while typing! + label.setFile(parent.with({ path: paths.join(parent.path, value || ' ') }), labelOptions); // update label icon while typing! }); - const value = stat.name || ''; const lastDot = value.lastIndexOf('.'); inputBox.value = value; @@ -683,18 +685,21 @@ export class FileSorter implements ISorter { } // Explorer Filter +interface CachedParsedExpression { + original: glob.IExpression; + parsed: glob.ParsedExpression; +} + export class FileFilter implements IFilter { - private static readonly MAX_SIBLINGS_FILTER_THRESHOLD = 2000; - - private hiddenExpressionPerRoot: Map; + private hiddenExpressionPerRoot: Map; private workspaceFolderChangeListener: IDisposable; constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService ) { - this.hiddenExpressionPerRoot = new Map(); + this.hiddenExpressionPerRoot = new Map(); this.registerListeners(); } @@ -707,9 +712,16 @@ export class FileFilter implements IFilter { let needsRefresh = false; this.contextService.getWorkspace().folders.forEach(folder => { const configuration = this.configurationService.getValue({ resource: folder.uri }); - const excludesConfig = (configuration && configuration.files && configuration.files.exclude) || Object.create(null); - needsRefresh = needsRefresh || !objects.equals(this.hiddenExpressionPerRoot.get(folder.uri.toString()), excludesConfig); - this.hiddenExpressionPerRoot.set(folder.uri.toString(), objects.deepClone(excludesConfig)); // do not keep the config, as it gets mutated under our hoods + const excludesConfig: glob.IExpression = (configuration && configuration.files && configuration.files.exclude) || Object.create(null); + + if (!needsRefresh) { + const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString()); + needsRefresh = !cached || !objects.equals(cached.original, excludesConfig); + } + + const excludesConfigCopy = objects.deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods + + this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression); }); return needsRefresh; @@ -724,18 +736,9 @@ export class FileFilter implements IFilter { return true; // always visible } - // Workaround for O(N^2) complexity (https://github.com/Microsoft/vscode/issues/9962) - let siblingsFn: () => string[]; - let siblingCount = stat.parent && stat.parent.getChildrenCount(); - if (siblingCount && siblingCount > FileFilter.MAX_SIBLINGS_FILTER_THRESHOLD) { - siblingsFn = () => void 0; - } else { - siblingsFn = () => stat.parent ? stat.parent.getChildrenNames() : void 0; - } - // Hide those that match Hidden Patterns - const expression = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()) || Object.create(null); - if (glob.match(expression, paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), siblingsFn)) { + const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); + if (cached && cached.parsed(paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), stat.name, name => !!stat.parent.getChild(name))) { return false; // hidden through pattern } diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index e949d6f6fe6..52205ce5b1d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -26,7 +26,7 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { EditorLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -176,8 +176,7 @@ export class OpenEditorsView extends ViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const title = dom.append(container, $('.title')); - dom.append(title, $('span', null, this.title)); + super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); this.dirtyCountElement = dom.append(count, $('.monaco-count-badge')); @@ -489,7 +488,7 @@ class OpenEditorActionRunner extends ActionRunner { } } -class OpenEditorsDelegate implements IDelegate { +class OpenEditorsDelegate implements IVirtualDelegate { public static readonly ITEM_HEIGHT = 22; @@ -595,6 +594,10 @@ class EditorGroupRenderer implements IRenderer this.onMarkerChanged(resources))); this._register(configurationService.onDidChangeConfiguration(e => { if (this.useFilesExclude && e.affectsConfiguration('files.exclude')) { diff --git a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts index 2345616c2bb..85e1d53e2ff 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts @@ -7,7 +7,7 @@ import * as paths from 'vs/base/common/paths'; import URI from 'vs/base/common/uri'; import { Range, IRange } from 'vs/editor/common/core/range'; -import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; +import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers'; import { IFilter, IMatch, or, matchesContiguousSubString, matchesPrefix, matchesFuzzy } from 'vs/base/common/filters'; import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import { Schemas } from 'vs/base/common/network'; @@ -15,6 +15,10 @@ import { groupBy, isFalsyOrEmpty, flatten } from 'vs/base/common/arrays'; import { values } from 'vs/base/common/map'; import * as glob from 'vs/base/common/glob'; import * as strings from 'vs/base/common/strings'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { CodeAction } from 'vs/editor/common/modes'; +import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; function compareUris(a: URI, b: URI) { if (a.toString() < b.toString()) { @@ -34,6 +38,7 @@ export class ResourceMarkers extends NodeWithId { private _name: string = null; private _path: string = null; + private _allFixesPromise: Promise; markers: Marker[] = []; isExcluded: boolean = false; @@ -42,7 +47,8 @@ export class ResourceMarkers extends NodeWithId { uriMatches: IMatch[] = []; constructor( - readonly uri: URI + readonly uri: URI, + private textModelService: ITextModelService ) { super(uri.toString()); } @@ -61,6 +67,37 @@ export class ResourceMarkers extends NodeWithId { return this._name; } + public getFixes(marker: Marker): Promise { + return this._getFixes(new Range(marker.range.startLineNumber, marker.range.startColumn, marker.range.endLineNumber, marker.range.endColumn)); + } + + public async hasFixes(marker: Marker): Promise { + if (!this._allFixesPromise) { + this._allFixesPromise = this._getFixes(); + } + const allFixes = await this._allFixesPromise; + if (allFixes.length) { + const markerKey = IMarkerData.makeKey(marker.raw); + for (const fix of allFixes) { + if (fix.diagnostics && fix.diagnostics.some(d => IMarkerData.makeKey(d) === markerKey)) { + return true; + } + } + } + return false; + } + + private async _getFixes(range?: Range): Promise { + const modelReference = await this.textModelService.createModelReference(this.uri); + if (modelReference) { + const model = modelReference.object.textEditorModel; + const codeActions = await getCodeActions(model, range ? range : model.getFullModelRange(), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }); + modelReference.dispose(); + return codeActions; + } + return []; + } + static compare(a: ResourceMarkers, b: ResourceMarkers): number { let [firstMarkerOfA] = a.markers; let [firstMarkerOfB] = b.markers; @@ -84,7 +121,7 @@ export class Marker extends NodeWithId { constructor( id: string, - readonly raw: IMarker, + readonly raw: IMarker ) { super(id); } @@ -185,7 +222,10 @@ export class MarkersModel { private _markersByResource: Map; private _filterOptions: FilterOptions; - constructor(markers: IMarker[] = []) { + constructor( + markers: IMarker[] = [], + @ITextModelService private textModelService: ITextModelService + ) { this._markersByResource = new Map(); this._filterOptions = new FilterOptions(); @@ -270,7 +310,7 @@ export class MarkersModel { private createResource(uri: URI, rawMarkers: IMarker[]): ResourceMarkers { const markers: Marker[] = []; - const resource = new ResourceMarkers(uri); + const resource = new ResourceMarkers(uri, this.textModelService); this.updateResource(resource); rawMarkers.forEach((rawMarker, index) => { 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/markersTreeController.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts index 930f8c9ee45..8fe527107df 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts @@ -7,14 +7,19 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as mouse from 'vs/base/browser/mouseEvent'; import * as tree from 'vs/base/parts/tree/browser/tree'; -import { MarkersModel } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { MarkersModel, Marker, ResourceMarkers } from 'vs/workbench/parts/markers/electron-browser/markersModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, Action } from 'vs/base/common/actions'; import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { localize } from 'vs/nls'; export class Controller extends WorkbenchTreeController { @@ -22,7 +27,10 @@ export class Controller extends WorkbenchTreeController { @IContextMenuService private contextMenuService: IContextMenuService, @IMenuService private menuService: IMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IBulkEditService private bulkEditService: IBulkEditService, + @ICommandService private commandService: ICommandService, + @IEditorService private editorService: IEditorService ) { super({}, configurationService); } @@ -45,17 +53,11 @@ export class Controller extends WorkbenchTreeController { public onContextMenu(tree: WorkbenchTree, element: any, event: tree.ContextMenuEvent): boolean { tree.setFocus(element, { preventOpenOnFocus: true }); - const actions = this._getMenuActions(tree); - if (!actions.length) { - return true; - } const anchor = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => { - return TPromise.as(actions); - }, + getActions: () => TPromise.wrap(this._getMenuActions(tree, element)), getActionItem: (action) => { const keybinding = this._keybindingService.lookupKeybinding(action.id); @@ -75,8 +77,20 @@ export class Controller extends WorkbenchTreeController { return true; } - private _getMenuActions(tree: WorkbenchTree): IAction[] { + private async _getMenuActions(tree: WorkbenchTree, element: any): Promise { const result: IAction[] = []; + + + if (element instanceof Marker) { + const quickFixActions = await this._getQuickFixActions(tree, element); + if (quickFixActions.length) { + result.push(...quickFixActions); + } else { + result.push(new Action('problems.no.fixes', localize('no fixes available', "No fixes available"), void 0, false)); + } + result.push(new Separator()); + } + const menu = this.menuService.createMenu(MenuId.ProblemsPanelContext, tree.contextKeyService); const groups = menu.getActions(); menu.dispose(); @@ -86,7 +100,38 @@ export class Controller extends WorkbenchTreeController { result.push(...actions); result.push(new Separator()); } + result.pop(); // remove last separator return result; } + + private async _getQuickFixActions(tree: WorkbenchTree, element: Marker): Promise { + const parent = tree.getNavigator(element).parent(); + if (parent instanceof ResourceMarkers) { + const codeActions = await parent.getFixes(element); + return codeActions.map(codeAction => new Action( + codeAction.command ? codeAction.command.id : codeAction.title, + codeAction.title, + void 0, + true, + () => { + return this.openFileAtMarker(element) + .then(() => applyCodeAction(codeAction, this.bulkEditService, this.commandService)); + })); + } + return []; + } + + public openFileAtMarker(element: Marker): TPromise { + const { resource, selection } = { resource: element.resource, selection: element.range }; + return this.editorService.openEditor({ + resource, + options: { + selection, + preserveFocus: true, + pinned: false, + revealIfVisible: true + }, + }, ACTIVE_GROUP).then(() => null); + } } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts index 5dee170ab08..4b8f13c73de 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; @@ -30,6 +31,7 @@ interface IResourceMarkersTemplateData { interface IMarkerTemplateData { icon: HTMLElement; + lightbulb: HTMLElement; source: HighlightedLabel; description: HighlightedLabel; lnCol: HTMLElement; @@ -96,7 +98,8 @@ export class Renderer implements IRenderer { constructor( @IInstantiationService private instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IWorkspaceContextService private contextService: IWorkspaceContextService ) { } @@ -178,6 +181,7 @@ export class Renderer implements IRenderer { private renderMarkerTemplate(container: HTMLElement): IMarkerTemplateData { const data: IMarkerTemplateData = Object.create(null); data.icon = dom.append(container, dom.$('.marker-icon')); + data.lightbulb = dom.append(container, dom.$('.icon.lightbulb')); data.source = new HighlightedLabel(dom.append(container, dom.$(''))); data.description = new HighlightedLabel(dom.append(container, dom.$('.marker-description'))); data.lnCol = dom.append(container, dom.$('span.marker-line')); @@ -200,14 +204,16 @@ export class Renderer implements IRenderer { if (templateData.resourceLabel instanceof FileLabel) { templateData.resourceLabel.setFile(element.uri, { matches: element.uriMatches }); } else { - templateData.resourceLabel.setLabel({ name: element.name, description: getPathLabel(element.uri, this.environmentService), resource: element.uri }, { matches: element.uriMatches }); + templateData.resourceLabel.setLabel({ name: element.name, description: getPathLabel(element.uri, this.environmentService, this.contextService), resource: element.uri }, { matches: element.uriMatches }); } (templateData).count.setCount(element.filteredCount); } private renderMarkerElement(tree: ITree, element: Marker, templateData: IMarkerTemplateData) { let marker = element.raw; + templateData.icon.className = 'icon ' + Renderer.iconClassNameFor(marker); + dom.removeClass(templateData.lightbulb, 'quick-fixes'); templateData.source.set(marker.source, element.sourceMatches); dom.toggleClass(templateData.source.element, 'marker-source', !!marker.source); @@ -216,11 +222,16 @@ export class Renderer implements IRenderer { templateData.description.element.title = marker.message; templateData.lnCol.textContent = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(marker.startLineNumber, marker.startColumn); + + const parent = tree.getNavigator(element).parent(); + if (parent instanceof ResourceMarkers) { + parent.hasFixes(element).then(hasFixes => dom.toggleClass(templateData.lightbulb, 'quick-fixes', hasFixes)); + } } private renderRelatedInfoElement(tree: ITree, element: RelatedInformation, templateData: IRelatedInformationTemplateData) { templateData.resourceLabel.set(paths.basename(element.raw.resource.fsPath), element.uriMatches); - templateData.resourceLabel.element.title = getPathLabel(element.raw.resource, this.environmentService); + templateData.resourceLabel.element.title = getPathLabel(element.raw.resource, this.environmentService, this.contextService); templateData.lnCol.textContent = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(element.raw.startLineNumber, element.raw.startColumn); templateData.description.set(element.raw.message, element.messageMatches); templateData.description.element.title = element.raw.message; @@ -256,9 +267,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/lightbulb-dark.svg b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb-dark.svg new file mode 100644 index 00000000000..520f78f3e55 --- /dev/null +++ b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/media/lightbulb.svg b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb.svg new file mode 100644 index 00000000000..b3596046616 --- /dev/null +++ b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..b351563aa6c 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; } @@ -160,4 +164,19 @@ .vs-dark .markers-panel .icon.info { background: url('status-info-inverse.svg') center center no-repeat; +} + +.markers-panel .icon.lightbulb { + background: url('lightbulb.svg') center/40% no-repeat; + position: absolute; + width: 24px; + height: 30px; +} + +.vs-dark .markers-panel .icon.lightbulb { + background: url('lightbulb-dark.svg') center/40% no-repeat; +} + +.markers-panel .icon.lightbulb:not(.quick-fixes) { + display: none; } \ No newline at end of file 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/markers/test/electron-browser/markersModel.test.ts b/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts index 7ed878b7ed2..b2bceb193b3 100644 --- a/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts +++ b/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts @@ -9,6 +9,8 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; import { MarkersModel, Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; class TestMarkersModel extends MarkersModel { @@ -22,6 +24,12 @@ class TestMarkersModel extends MarkersModel { suite('MarkersModel Test', () => { + let instantiationService: IInstantiationService; + + setup(() => { + instantiationService = workbenchInstantiationService(); + }); + test('getFilteredResource return markers grouped by resource', function () { const marker1 = aMarker('res1'); const marker2 = aMarker('res2'); @@ -29,7 +37,7 @@ suite('MarkersModel Test', () => { const marker4 = aMarker('res3'); const marker5 = aMarker('res4'); const marker6 = aMarker('res2'); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6]); const actuals = testObject.filteredResources; @@ -61,7 +69,7 @@ suite('MarkersModel Test', () => { const marker4 = aMarker('b/res3'); const marker5 = aMarker('res4'); const marker6 = aMarker('c/res2', MarkerSeverity.Info); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6]); const actuals = testObject.resources; @@ -80,7 +88,7 @@ suite('MarkersModel Test', () => { const marker4 = aMarker('b/res3'); const marker5 = aMarker('res4'); const marker6 = aMarker('c/res2'); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6]); const actuals = testObject.resources; @@ -108,7 +116,7 @@ suite('MarkersModel Test', () => { const marker13 = aWarningWithRange(5); const marker14 = anErrorWithRange(4); const marker15 = anErrorWithRange(8, 2, 8, 4); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6, marker7, marker8, marker9, marker10, marker11, marker12, marker13, marker14, marker15]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6, marker7, marker8, marker9, marker10, marker11, marker12, marker13, marker14, marker15]); const actuals = testObject.resources[0].markers; @@ -132,19 +140,19 @@ suite('MarkersModel Test', () => { test('toString()', function () { let marker = aMarker('a/res1'); marker.code = '1234'; - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker).toString()); marker = aMarker('a/res2', MarkerSeverity.Warning); - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker).toString()); marker = aMarker('a/res2', MarkerSeverity.Info, 1, 2, 1, 8, 'Info', ''); - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker).toString()); marker = aMarker('a/res2', MarkerSeverity.Hint, 1, 2, 1, 8, 'Ignore message', 'Ignore'); - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker).toString()); marker = aMarker('a/res2', MarkerSeverity.Warning, 1, 2, 1, 8, 'Warning message', '', [{ startLineNumber: 2, startColumn: 5, endLineNumber: 2, endColumn: 10, message: 'some info', resource: URI.file('a/res3') }]); - const testObject = new Marker('', marker); + const testObject = instantiationService.createInstance(Marker, '', marker); testObject.resourceRelatedInformation = marker.relatedInformation.map(r => new RelatedInformation('', r)); assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path, relatedInformation: marker.relatedInformation.map(r => ({ ...r, resource: r.resource.path })) }, null, '\t'), testObject.toString()); }); 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 3650e6cf8cb..3634c35da04 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts +++ b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts @@ -18,7 +18,7 @@ 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'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { escape } from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; @@ -352,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; @@ -515,7 +518,7 @@ export class OutlinePanel extends ViewletPanel { this._progressBar.stop().hide(); - if (oldModel && oldModel.adopt(model)) { + if (oldModel && oldModel.merge(model)) { this._tree.refresh(undefined, true); model = oldModel; @@ -564,9 +567,7 @@ export class OutlinePanel extends ViewletPanel { } this._editorDisposables.push(this._input.onDidChange(onInputValueChanged)); - this._editorDisposables.push({ - dispose: () => this._contextKeyFiltered.reset() - }); + this._editorDisposables.push(toDisposable(() => this._contextKeyFiltered.reset())); // feature: reveal outline selection in editor // on change -> reveal/select defining range diff --git a/src/vs/workbench/parts/output/browser/outputActions.ts b/src/vs/workbench/parts/output/browser/outputActions.ts index c73e611e656..8543c9bb2b0 100644 --- a/src/vs/workbench/parts/output/browser/outputActions.ts +++ b/src/vs/workbench/parts/output/browser/outputActions.ts @@ -116,7 +116,7 @@ export class SwitchOutputActionItem extends SelectActionItem { @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, [], 0, contextViewService); + super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputs', 'Outputs') }); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); this.toDispose.push(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions(this.outputService.getActiveChannel().id))); diff --git a/src/vs/workbench/parts/output/electron-browser/output.contribution.ts b/src/vs/workbench/parts/output/electron-browser/output.contribution.ts index 63be0b0f1c2..4442ede409c 100644 --- a/src/vs/workbench/parts/output/electron-browser/output.contribution.ts +++ b/src/vs/workbench/parts/output/electron-browser/output.contribution.ts @@ -187,3 +187,12 @@ CommandsRegistry.registerCommand(COMMAND_OPEN_LOG_VIEWER, function (accessor: Se } return null; }); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: ToggleOutputAction.ID, + title: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output") + }, + order: 1 +}); 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..c071be384ec 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -32,7 +32,7 @@ import { import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -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`; @@ -594,7 +614,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor } } -class Delegate implements IDelegate { +class Delegate implements IVirtualDelegate { getHeight(element: IListEntry) { if (element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) { @@ -647,6 +667,9 @@ class KeybindingHeaderRenderer implements IRenderer { renderElement(entry: IListEntry, index: number, template: any): void { } + disposeElement(): void { + } + disposeTemplate(template: any): void { } } @@ -684,6 +707,8 @@ class KeybindingItemRenderer implements IRenderer .settings-header { - padding-left: 5px; - padding-right: 5px; + padding-left: 17px; + padding-right: 11px; box-sizing: border-box; margin: auto; } @@ -71,7 +72,7 @@ } .settings-editor > .settings-header > .settings-header-controls { - margin-top: 3px; + margin-top: 8px; height: 30px; display: flex; } @@ -113,10 +114,16 @@ outline: none !important; } +.settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper, +.settings-editor > .settings-body > .settings-tree-container .setting-measure-container { + /** Allocate space for the scrollbar */ + width: calc(100% - 11px) +} + + .settings-editor > .settings-body .settings-toc-container { width: 175px; margin-right: 5px; - padding-top: 5px; } .settings-editor > .settings-body .settings-toc-container.hidden { @@ -155,8 +162,8 @@ } .settings-editor > .settings-body > .settings-tree-container .setting-item { - padding-top: 9px; - padding-bottom: 13px; + padding-top: 11px; + padding-bottom: 15px; box-sizing: border-box; cursor: default; white-space: normal; @@ -197,11 +204,19 @@ opacity: 0.9; margin-top: 3px; overflow: hidden; - white-space: pre; text-overflow: ellipsis; height: 18px; } +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description * { + margin: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description code { + line-height: 15px; /** For some reason, this is needed, otherwise will take up 20px height */ + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; +} + .settings-editor > .settings-body > .settings-tree-container .setting-measure-container.monaco-tree-row { position: absolute; visibility: hidden; @@ -210,7 +225,6 @@ .settings-editor > .settings-body > .settings-tree-container .setting-item.is-expanded .setting-item-description, .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-measure-helper .setting-item-description { height: initial; - white-space: pre-wrap; } .settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-item-value-description { @@ -242,17 +256,17 @@ display: flex; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-item-control.setting-type-number { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-number .setting-item-value > .setting-item-control { min-width: 200px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-item-control.setting-type-enum, -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-item-control.setting-type-string { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-enum .setting-item-value > .setting-item-control, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-text .setting-item-value > .setting-item-control { flex: 1; min-width: initial; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-item-control.setting-type-enum > select { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-enum .setting-item-value > .setting-item-control > select { width: 100%; } @@ -296,7 +310,7 @@ } .settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { - padding-top: 7px; + padding-top: 4px; } .settings-editor > .settings-body .settings-feedback-button { diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 0211aa5d897..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)); 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..8b6d31130d0 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -9,7 +9,7 @@ import * as arrays from 'vs/base/common/arrays'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import * as collections from 'vs/base/common/collections'; -import { Color } from 'vs/base/common/color'; +import { Color, RGBA } from 'vs/base/common/color'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, focusBorder, foreground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -33,13 +33,19 @@ import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbenc import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, NonExpandableTree, resolveExtensionsSettings, resolveSettingsTree, SearchResultIdx, SearchResultModel, SettingsAccessibilityProvider, SettingsDataSource, SettingsRenderer, SettingsTreeController, SettingsTreeElement, SettingsTreeFilter, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTree'; import { TOCDataSource, TOCRenderer, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_FIRST_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, IPreferencesSearchService, ISearchProvider, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_TOC_ROW_FOCUS } from 'vs/workbench/parts/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_FIRST_ROW_FOCUS, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences'; import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; const $ = DOM.$; +export const settingItemInactiveSelectionBorder = registerColor('settings.inactiveSelectedItemBorder', { + dark: '#3F3F46', + light: '#CCCEDB', + hc: null +}, localize('settingItemInactiveSelectionBorder', "The color of the selected setting row border, when the settings list does not have focus.")); + export class SettingsEditor2 extends BaseEditor { public static readonly ID: string = 'workbench.editor.settings2'; @@ -81,6 +87,9 @@ export class SettingsEditor2 extends BaseEditor { private inSettingsEditorContextKey: IContextKey; private searchFocusContextKey: IContextKey; + /** Don't spam warnings */ + private hasWarnedMissingSettings: boolean; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService private configurationService: IConfigurationService, @@ -189,7 +198,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())); @@ -233,6 +243,16 @@ export class SettingsEditor2 extends BaseEditor { this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked())); } + private revealSetting(settingName: string): void { + const element = this.settingsTreeModel.getElementByName(settingName); + if (element) { + this.settingsTree.setSelection([element]); + this.settingsTree.setFocus(element); + this.settingsTree.reveal(element, 0); + this.settingsTree.domFocus(); + } + } + private openSettingsFile(): TPromise { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; @@ -313,6 +333,7 @@ export class SettingsEditor2 extends BaseEditor { const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer); this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value))); this._register(renderer.onDidOpenSettings(() => this.openSettingsFile())); + this._register(renderer.onDidClickSettingLink(settingName => this.revealSetting(settingName))); const treeClass = 'settings-editor-tree'; this.settingsTree = this.instantiationService.createInstance(NonExpandableTree, this.settingsTreeContainer, @@ -332,15 +353,23 @@ export class SettingsEditor2 extends BaseEditor { }); this._register(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - const activeBorderColor = theme.getColor(listActiveSelectionBackground); + const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`); } - const inactiveBorderColor = theme.getColor(listInactiveSelectionBackground); + const inactiveBorderColor = theme.getColor(settingItemInactiveSelectionBorder); if (inactiveBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused {outline: solid 1px ${inactiveBorderColor}; outline-offset: -1px; }`); } + + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + // Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid + // applying an opacity to the link color. + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, .7)); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description { color: ${fgWithOpacity}; }`); + } })); this.settingsTree.getHTMLElement().classList.add(treeClass); @@ -581,9 +610,22 @@ export class SettingsEditor2 extends BaseEditor { private onConfigUpdate(): TPromise { const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core'); - const resolvedSettingsRoot = resolveSettingsTree(tocData, dividedGroups.core); + const settingsResult = resolveSettingsTree(tocData, dividedGroups.core); + const resolvedSettingsRoot = settingsResult.tree; + + // Warn for settings not included in layout + if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) { + let settingKeyList = []; + settingsResult.leftoverSettings.forEach(s => { + settingKeyList.push(s.key); + }); + + this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`); + this.hasWarnedMissingSettings = true; + } + const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core); - resolvedSettingsRoot.children.unshift(commonlyUsed); + resolvedSettingsRoot.children.unshift(commonlyUsed.tree); resolvedSettingsRoot.children.push(resolveExtensionsSettings(dividedGroups.extension || [])); @@ -692,7 +734,10 @@ export class SettingsEditor2 extends BaseEditor { // Count unique results const counts = {}; const filterResult = results[SearchResultIdx.Local]; - counts['filterResult'] = filterResult.filterMatches.length; + if (filterResult) { + counts['filterResult'] = filterResult.filterMatches.length; + } + if (nlpResult) { counts['nlpResult'] = nlpResult.filterMatches.length; } diff --git a/src/vs/workbench/parts/preferences/browser/settingsLayout.ts b/src/vs/workbench/parts/preferences/browser/settingsLayout.ts index caf7d55ff89..ef3cccf1611 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsLayout.ts @@ -61,7 +61,7 @@ export const tocData: ITOCEntry = { { id: 'editor/suggestions', label: localize('suggestions', "Suggestions"), - settings: ['editor.*suggestion*'] + settings: ['editor.*suggest*'] }, { id: 'editor/files', @@ -84,6 +84,11 @@ export const tocData: ITOCEntry = { label: localize('appearance', "Appearance"), settings: ['workbench.activityBar.*', 'workbench.*color*', 'workbench.fontAliasing', 'workbench.iconTheme', 'workbench.sidebar.location', 'workbench.*.visible', 'workbench.tips.enabled', 'workbench.tree.*', 'workbench.view.*'] }, + { + id: 'workbench/breadcrumbs', + label: localize('breadcrumbs', "Breadcrumbs"), + settings: ['breadcrumbs.*'] + }, { id: 'workbench/editor', label: localize('editorManagement', "Editor Management"), @@ -128,7 +133,7 @@ export const tocData: ITOCEntry = { children: [ { id: 'features/explorer', - label: localize('fileExplorer', "File Explorer"), + label: localize('fileExplorer', "Explorer"), settings: ['explorer.*', 'outline.*'] }, { diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index a30f9616f35..5edea518522 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -11,25 +12,27 @@ import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import * as arrays from 'vs/base/common/arrays'; -import { Color } from 'vs/base/common/color'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; -import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IAccessibilityProvider, IDataSource, IFilter, IRenderer, ITree } from 'vs/base/parts/tree/browser/tree'; +import { IAccessibilityProvider, IDataSource, IFilter, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { registerColor, selectBackground, selectBorder } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { inputBackground, inputBorder, inputForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { attachInputBoxStyler, attachSelectBoxStyler, attachButtonStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { SettingsTarget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { ITOCEntry } from 'vs/workbench/parts/preferences/browser/settingsLayout'; import { ISearchResult, ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; +import { Color } from 'vs/base/common/color'; const $ = DOM.$; @@ -39,25 +42,46 @@ export const modifiedItemForeground = registerColor('settings.modifiedItemForegr hc: '#73C991' }, localize('modifiedItemForeground', "(For settings editor preview) The foreground color for a modified setting.")); +// Enum control colors +export const settingsSelectBackground = registerColor('settings.dropdownBackground', { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsDropdownBackground', "Settings editor dropdown background.")); +export const settingsSelectForeground = registerColor('settings.dropdownForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsDropdownForeground', "Settings editor dropdown foreground.")); +export const settingsSelectBorder = registerColor('settings.dropdownBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsDropdownBorder', "Settings editor dropdown border.")); + +// Bool control colors +export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsCheckboxBackground', "Settings editor checkbox background.")); +export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsCheckboxForeground', "Settings editor checkbox foreground.")); +export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); + +// Text control colors +export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); +export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); + +// Number control colors +export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); +export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); + registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const modifiedItemForegroundColor = theme.getColor(modifiedItemForeground); if (modifiedItemForegroundColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-is-configured-label { color: ${modifiedItemForegroundColor}; }`); } -}); -registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - // TODO@roblou Hacks! Make checkbox background themeable - const selectBackgroundColor = theme.getColor(selectBackground); - if (selectBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { background-color: ${selectBackgroundColor} !important; }`); + const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); + if (checkboxBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { background-color: ${checkboxBackgroundColor} !important; }`); } - // TODO@roblou Hacks! Use proper inputbox theming instead of !important - const selectBorderColor = theme.getColor(selectBorder); - if (selectBorderColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { border-color: ${selectBorderColor} !important; }`); - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-control > .monaco-inputbox { border: solid 1px ${selectBorderColor} !important; }`); + const checkboxBorderColor = theme.getColor(settingsCheckboxBorder); + if (checkboxBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { border-color: ${checkboxBorderColor} !important; }`); + } + + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description a { color: ${link}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description a > code { color: ${link}; }`); } }); @@ -76,15 +100,13 @@ export class SettingsTreeGroupElement extends SettingsTreeElement { export class SettingsTreeSettingElement extends SettingsTreeElement { setting: ISetting; - isExpanded: boolean; displayCategory: string; displayLabel: string; value: any; isConfigured: boolean; overriddenScopeList: string[]; description: string; - valueType?: string | string[]; - enum?: string[]; + valueType: 'enum' | 'string' | 'integer' | 'number' | 'boolean' | 'complex'; } export interface ITOCEntry { @@ -97,6 +119,7 @@ export interface ITOCEntry { export class SettingsTreeModel { private _root: SettingsTreeGroupElement; private _treeElementsById = new Map(); + private _treeElementsBySettingName = new Map(); constructor( private _viewState: ISettingsEditorViewState, @@ -125,6 +148,10 @@ export class SettingsTreeModel { return this._treeElementsById.get(id); } + getElementByName(name: string): SettingsTreeElement { + return this._treeElementsBySettingName.get(name); + } + private createSettingsTreeGroupElement(tocEntry: ITOCEntry, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement { const element = new SettingsTreeGroupElement(); element.id = tocEntry.id; @@ -153,6 +180,7 @@ export class SettingsTreeModel { private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement { const element = createSettingsTreeSettingElement(setting, parent, this._viewState.settingsTarget, this._configurationService); this._treeElementsById.set(element.id, element); + this._treeElementsBySettingName.set(setting.key, element); return element; } } @@ -182,14 +210,18 @@ function createSettingsTreeSettingElement(setting: ISetting, parent: any, settin element.setting = setting; element.displayLabel = displayKeyFormat.label; element.displayCategory = displayKeyFormat.category; - element.isExpanded = false; element.value = displayValue; element.isConfigured = isConfigured; element.overriddenScopeList = overriddenScopeList; element.description = setting.description.join('\n'); - element.enum = setting.enum; - element.valueType = setting.type; + + element.valueType = (setting.enum && (setting.type === 'string' || !setting.type)) ? 'enum' : + setting.type === 'string' ? 'string' : + setting.type === 'integer' ? 'integer' : + setting.type === 'number' ? 'number' : + setting.type === 'boolean' ? 'boolean' : + 'complex'; return element; } @@ -205,8 +237,12 @@ function inspectSetting(key: string, target: SettingsTarget, configurationServic return { isConfigured, inspected, targetSelector }; } -export function resolveSettingsTree(tocData: ITOCEntry, coreSettingsGroups: ISettingsGroup[]): ITOCEntry { - return _resolveSettingsTree(tocData, getFlatSettings(coreSettingsGroups)); +export function resolveSettingsTree(tocData: ITOCEntry, coreSettingsGroups: ISettingsGroup[]): { tree: ITOCEntry, leftoverSettings: Set } { + const allSettings = getFlatSettings(coreSettingsGroups); + return { + tree: _resolveSettingsTree(tocData, allSettings), + leftoverSettings: allSettings + }; } export function resolveExtensionsSettings(groups: ISettingsGroup[]): ITOCEntry { @@ -280,7 +316,9 @@ function getFlatSettings(settingsGroups: ISettingsGroup[]) { for (let group of settingsGroups) { for (let section of group.sections) { for (let s of section.settings) { - result.add(s); + if (!s.overrides || !s.overrides.length) { + result.add(s); + } } } } @@ -394,8 +432,8 @@ interface IDisposableTemplate { toDispose: IDisposable[]; } -interface ISettingItemTemplate extends IDisposableTemplate { - parent: HTMLElement; +interface ISettingItemTemplate extends IDisposableTemplate { + onChange?: (value: T) => void; containerElement: HTMLElement; categoryElement: HTMLElement; @@ -406,17 +444,22 @@ interface ISettingItemTemplate extends IDisposableTemplate { otherOverridesElement: HTMLElement; } -interface ISettingBoolItemTemplate extends IDisposableTemplate { - parent: HTMLElement; - - onChange?: (newState: boolean) => void; - containerElement: HTMLElement; - categoryElement: HTMLElement; - labelElement: HTMLElement; - descriptionElement: HTMLElement; +interface ISettingBoolItemTemplate extends ISettingItemTemplate { checkbox: Checkbox; - isConfiguredElement: HTMLElement; - otherOverridesElement: HTMLElement; +} + +interface ISettingTextItemTemplate extends ISettingItemTemplate { + inputBox: InputBox; +} + +type ISettingNumberItemTemplate = ISettingTextItemTemplate; + +interface ISettingEnumItemTemplate extends ISettingItemTemplate { + selectBox: SelectBox; +} + +interface ISettingComplexItemTemplate extends ISettingItemTemplate { + button: Button; } interface IGroupTitleTemplate extends IDisposableTemplate { @@ -424,8 +467,11 @@ interface IGroupTitleTemplate extends IDisposableTemplate { parent: HTMLElement; } -const SETTINGS_ELEMENT_TEMPLATE_ID = 'settings.entry.template'; +const SETTINGS_TEXT_TEMPLATE_ID = 'settings.text.template'; +const SETTINGS_NUMBER_TEMPLATE_ID = 'settings.number.template'; +const SETTINGS_ENUM_TEMPLATE_ID = 'settings.enum.template'; const SETTINGS_BOOL_TEMPLATE_ID = 'settings.bool.template'; +const SETTINGS_COMPLEX_TEMPLATE_ID = 'settings.complex.template'; const SETTINGS_GROUP_ELEMENT_TEMPLATE_ID = 'settings.group.template'; export interface ISettingChangeEvent { @@ -435,8 +481,9 @@ export interface ISettingChangeEvent { export class SettingsRenderer implements IRenderer { - private static readonly SETTING_ROW_HEIGHT = 94; - private static readonly SETTING_BOOL_ROW_HEIGHT = 61; + private static readonly SETTING_ROW_HEIGHT = 98; + private static readonly SETTING_BOOL_ROW_HEIGHT = 65; + public static readonly MAX_ENUM_DESCRIPTIONS = 10; private readonly _onDidChangeSetting: Emitter = new Emitter(); public readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; @@ -444,12 +491,16 @@ export class SettingsRenderer implements IRenderer { private readonly _onDidOpenSettings: Emitter = new Emitter(); public readonly onDidOpenSettings: Event = this._onDidOpenSettings.event; + private readonly _onDidClickSettingLink: Emitter = new Emitter(); + public readonly onDidClickSettingLink: Event = this._onDidClickSettingLink.event; + private measureContainer: HTMLElement; constructor( _measureContainer: HTMLElement, @IThemeService private themeService: IThemeService, - @IContextViewService private contextViewService: IContextViewService + @IContextViewService private contextViewService: IContextViewService, + @IOpenerService private readonly openerService: IOpenerService, ) { this.measureContainer = DOM.append(_measureContainer, $('.setting-measure-container.monaco-tree-row')); } @@ -457,7 +508,7 @@ export class SettingsRenderer implements IRenderer { getHeight(tree: ITree, element: SettingsTreeElement): number { if (element instanceof SettingsTreeGroupElement) { if (element.isFirstGroup) { - return 31; + return 28; } return 40 + (7 * element.level); @@ -506,7 +557,19 @@ export class SettingsRenderer implements IRenderer { return SETTINGS_BOOL_TEMPLATE_ID; } - return SETTINGS_ELEMENT_TEMPLATE_ID; + if (element.valueType === 'integer' || element.valueType === 'number') { + return SETTINGS_NUMBER_TEMPLATE_ID; + } + + if (element.valueType === 'string') { + return SETTINGS_TEXT_TEMPLATE_ID; + } + + if (element.valueType === 'enum') { + return SETTINGS_ENUM_TEMPLATE_ID; + } + + return SETTINGS_COMPLEX_TEMPLATE_ID; } return ''; @@ -517,14 +580,26 @@ export class SettingsRenderer implements IRenderer { return this.renderGroupTitleTemplate(container); } - if (templateId === SETTINGS_ELEMENT_TEMPLATE_ID) { - return this.renderSettingTemplate(tree, container); + if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { + return this.renderSettingTextTemplate(tree, container); + } + + if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { + return this.renderSettingNumberTemplate(tree, container); } if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { return this.renderSettingBoolTemplate(tree, container); } + if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { + return this.renderSettingEnumTemplate(tree, container); + } + + if (templateId === SETTINGS_COMPLEX_TEMPLATE_ID) { + return this.renderSettingComplexTemplate(tree, container); + } + return null; } @@ -540,8 +615,9 @@ export class SettingsRenderer implements IRenderer { return template; } - private renderSettingTemplate(tree: ITree, container: HTMLElement): ISettingItemTemplate { + private renderCommonTemplate(tree: ITree, container: HTMLElement, typeClass: string): ISettingItemTemplate { DOM.addClass(container, 'setting-item'); + DOM.addClass(container, 'setting-item-' + typeClass); const titleElement = DOM.append(container, $('.setting-item-title')); const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); @@ -551,12 +627,10 @@ export class SettingsRenderer implements IRenderer { const descriptionElement = DOM.append(container, $('.setting-item-description')); const valueElement = DOM.append(container, $('.setting-item-value')); - const controlElement = DOM.append(valueElement, $('div')); - const resetButtonElement = DOM.append(valueElement, $('.reset-button-container')); + const controlElement = DOM.append(valueElement, $('div.setting-item-control')); const toDispose = []; const template: ISettingItemTemplate = { - parent: container, toDispose, containerElement: container, @@ -570,7 +644,6 @@ export class SettingsRenderer implements IRenderer { // Prevent clicks from being handled by list toDispose.push(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); - toDispose.push(DOM.addDisposableListener(resetButtonElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); toDispose.push(DOM.addStandardDisposableListener(valueElement, 'keydown', (e: StandardKeyboardEvent) => { if (e.keyCode === KeyCode.Escape) { @@ -582,6 +655,58 @@ export class SettingsRenderer implements IRenderer { return template; } + private renderSettingTextTemplate(tree: ITree, container: HTMLElement, type = 'text'): ISettingTextItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'text'); + + const inputBox = new InputBox(common.controlElement, this.contextViewService); + common.toDispose.push(inputBox); + common.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + })); + common.toDispose.push( + inputBox.onDidChange(e => { + if (template.onChange) { + template.onChange(e); + } + })); + common.toDispose.push(inputBox); + + const template: ISettingTextItemTemplate = { + ...common, + inputBox + }; + + return template; + } + + private renderSettingNumberTemplate(tree: ITree, container: HTMLElement): ISettingNumberItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'number'); + + const inputBox = new InputBox(common.controlElement, this.contextViewService); + common.toDispose.push(inputBox); + common.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { + inputBackground: settingsNumberInputBackground, + inputForeground: settingsNumberInputForeground, + inputBorder: settingsNumberInputBorder + })); + common.toDispose.push( + inputBox.onDidChange(e => { + if (template.onChange) { + template.onChange(e); + } + })); + common.toDispose.push(inputBox); + + const template: ISettingNumberItemTemplate = { + ...common, + inputBox + }; + + return template; + } + private renderSettingBoolTemplate(tree: ITree, container: HTMLElement): ISettingBoolItemTemplate { DOM.addClass(container, 'setting-item'); DOM.addClass(container, 'setting-item-bool'); @@ -607,12 +732,12 @@ export class SettingsRenderer implements IRenderer { })); const template: ISettingBoolItemTemplate = { - parent: container, toDispose, containerElement: container, categoryElement, labelElement, + controlElement, checkbox, descriptionElement, isConfiguredElement, @@ -632,18 +757,62 @@ export class SettingsRenderer implements IRenderer { return template; } + private renderSettingEnumTemplate(tree: ITree, container: HTMLElement): ISettingEnumItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'enum'); + + const selectBox = new SelectBox([], undefined, this.contextViewService); + common.toDispose.push(selectBox); + common.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService, { + selectBackground: settingsSelectBackground, + selectForeground: settingsSelectForeground, + selectBorder: settingsSelectBorder + })); + selectBox.render(common.controlElement); + + common.toDispose.push( + selectBox.onDidSelect(e => { + if (template.onChange) { + template.onChange(e.index); + } + })); + + const template: ISettingEnumItemTemplate = { + ...common, + selectBox + }; + + return template; + } + + private renderSettingComplexTemplate(tree: ITree, container: HTMLElement): ISettingComplexItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'complex'); + + const openSettingsButton = new Button(common.controlElement, { title: true, buttonBackground: null, buttonHoverBackground: null }); + common.toDispose.push(openSettingsButton); + common.toDispose.push(openSettingsButton.onDidClick(() => this._onDidOpenSettings.fire())); + openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); + openSettingsButton.element.classList.add('edit-in-settings-button'); + + common.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, { + buttonBackground: Color.transparent.toString(), + buttonHoverBackground: Color.transparent.toString(), + buttonForeground: 'foreground' + })); + + const template: ISettingComplexItemTemplate = { + ...common, + button: openSettingsButton + }; + + return template; + } + renderElement(tree: ITree, element: SettingsTreeElement, templateId: string, template: any): void { - if (templateId === SETTINGS_ELEMENT_TEMPLATE_ID) { - return this.renderSettingElement(tree, element, template); - } - - if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { - return this.renderSettingElement(tree, element, template); - } - if (templateId === SETTINGS_GROUP_ELEMENT_TEMPLATE_ID) { return this.renderGroupElement(element, template); } + + return this.renderSettingElement(tree, element, templateId, template); } private renderGroupElement(element: SettingsTreeGroupElement, template: IGroupTitleTemplate): void { @@ -663,12 +832,12 @@ export class SettingsRenderer implements IRenderer { return selectedElement && selectedElement.id === element.id; } - private renderSettingElement(tree: ITree, element: SettingsTreeSettingElement, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { + private renderSettingElement(tree: ITree, element: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { const isSelected = !!this.elementIsSelected(tree, element); const setting = element.setting; - DOM.toggleClass(template.parent, 'is-configured', element.isConfigured); - DOM.toggleClass(template.parent, 'is-expanded', isSelected); + DOM.toggleClass(template.containerElement, 'is-configured', element.isConfigured); + DOM.toggleClass(template.containerElement, 'is-expanded', isSelected); template.containerElement.id = element.id.replace(/\./g, '_'); const titleTooltip = setting.key; @@ -677,9 +846,41 @@ export class SettingsRenderer implements IRenderer { template.labelElement.textContent = element.displayLabel; template.labelElement.title = titleTooltip; - template.descriptionElement.textContent = element.description; - this.renderValue(element, isSelected, template); + let enumDescriptionText = ''; + if (element.valueType === 'string' && element.setting.enumDescriptions && element.setting.enum && element.setting.enum.length < SettingsRenderer.MAX_ENUM_DESCRIPTIONS) { + enumDescriptionText = '\n' + element.setting.enumDescriptions + .map((desc, i) => desc ? + ` - \`${element.setting.enum[i]}\` : + ${desc}` : ` - \`${element.setting.enum[i]}\``) + .filter(desc => !!desc) + .join('\n'); + } + + // Rewrite `#editor.fontSize#` to link format + const descriptionText = (element.description + enumDescriptionText) + .replace(/`#(.*)#`/g, (match, settingName) => `[\`${settingName}\`](#${settingName})`); + + const renderedDescription = renderMarkdown({ value: descriptionText }, { + actionHandler: { + callback: (content: string) => { + if (startsWith(content, '#')) { + this._onDidClickSettingLink.fire(content.substr(1)); + } else { + this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError); + } + }, + disposeables: template.toDispose + } + }); + renderedDescription.classList.add('setting-item-description-markdown'); + template.descriptionElement.innerHTML = ''; + template.descriptionElement.appendChild(renderedDescription); + (renderedDescription.querySelectorAll('a')).forEach(aElement => { + aElement.tabIndex = isSelected ? 0 : -1; + }); + + this.renderValue(element, isSelected, templateId, template); template.isConfiguredElement.textContent = element.isConfigured ? localize('configured', "Modified") : ''; @@ -694,34 +895,19 @@ export class SettingsRenderer implements IRenderer { } } - private renderValue(element: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { + private renderValue(element: SettingsTreeSettingElement, isSelected: boolean, templateId: string, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { const onChange = value => this._onDidChangeSetting.fire({ key: element.setting.key, value }); - if (element.valueType === 'boolean') { + if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { + this.renderEnum(element, isSelected, template, onChange); + } else if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { + this.renderText(element, isSelected, template, onChange); + } else if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { + this.renderNumber(element, isSelected, template, onChange); + } else if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { this.renderBool(element, isSelected, template, onChange); - } else { - return this._renderValue(element, isSelected, template, onChange); - } - } - - private _renderValue(element: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, onChange: (value: any) => void): void { - const valueControlElement = template.controlElement; - valueControlElement.innerHTML = ''; - - valueControlElement.setAttribute('class', 'setting-item-control'); - if (element.enum && (element.valueType === 'string' || !element.valueType)) { - valueControlElement.classList.add('setting-type-enum'); - this.renderEnum(element, isSelected, template, valueControlElement, onChange); - } else if (element.valueType === 'string') { - valueControlElement.classList.add('setting-type-string'); - this.renderText(element, isSelected, template, valueControlElement, onChange); - } else if (element.valueType === 'number' || element.valueType === 'integer') { - valueControlElement.classList.add('setting-type-number'); - const parseFn = element.valueType === 'integer' ? parseInt : parseFloat; - this.renderText(element, isSelected, template, valueControlElement, value => onChange(parseFn(value))); - } else { - valueControlElement.classList.add('setting-type-complex'); - this.renderEditInSettingsJson(element, isSelected, template, valueControlElement); + } else if (templateId === SETTINGS_COMPLEX_TEMPLATE_ID) { + this.renderEditInSettingsJson(element, isSelected, template); } } @@ -733,44 +919,44 @@ export class SettingsRenderer implements IRenderer { template.checkbox.domNode.tabIndex = isSelected ? 0 : -1; } - private renderEnum(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement, onChange: (value: string) => void): void { - const idx = dataElement.enum.indexOf(dataElement.value); - const displayOptions = dataElement.enum.map(escapeInvisibleChars); - const selectBox = new SelectBox(displayOptions, idx, this.contextViewService); - template.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService)); - selectBox.render(element); - if (element.firstElementChild) { - element.firstElementChild.setAttribute('tabindex', isSelected ? '0' : '-1'); + private renderEnum(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void { + const displayOptions = getDisplayEnumOptions(dataElement.setting); + template.selectBox.setOptions(displayOptions); + + const label = dataElement.displayCategory + ' ' + dataElement.displayLabel; + template.selectBox.setAriaLabel(label); + + const idx = dataElement.setting.enum.indexOf(dataElement.value); + template.onChange = null; + template.selectBox.select(idx); + template.onChange = idx => onChange(dataElement.setting.enum[idx]); + + if (template.controlElement.firstElementChild) { + template.controlElement.firstElementChild.setAttribute('tabindex', isSelected ? '0' : '-1'); } - template.toDispose.push( - selectBox.onDidSelect(e => onChange(dataElement.enum[e.index]))); } - private renderText(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement, onChange: (value: string) => void): void { - const inputBox = new InputBox(element, this.contextViewService); - template.toDispose.push(attachInputBoxStyler(inputBox, this.themeService)); - template.toDispose.push(inputBox); - inputBox.value = dataElement.value; - inputBox.inputElement.tabIndex = isSelected ? 0 : -1; - - template.toDispose.push( - inputBox.onDidChange(e => onChange(e))); + private renderText(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingTextItemTemplate, onChange: (value: string) => void): void { + template.onChange = null; + template.inputBox.value = dataElement.value; + template.onChange = value => onChange(value); + template.inputBox.inputElement.tabIndex = isSelected ? 0 : -1; } - private renderEditInSettingsJson(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingItemTemplate, element: HTMLElement): void { - const openSettingsButton = new Button(element, { title: true, buttonBackground: null, buttonHoverBackground: null }); - openSettingsButton.onDidClick(() => this._onDidOpenSettings.fire()); - openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); - openSettingsButton.element.classList.add('edit-in-settings-button'); - openSettingsButton.element.tabIndex = isSelected ? 0 : -1; + private renderNumber(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingTextItemTemplate, onChange: (value: number) => void): void { + template.onChange = null; + template.inputBox.value = dataElement.value; + template.onChange = value => onChange(parseFn(value)); + template.inputBox.inputElement.tabIndex = isSelected ? 0 : -1; - template.toDispose.push(openSettingsButton); - template.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, { - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: 'foreground' - })); + const parseFn = dataElement.valueType === 'integer' ? parseInt : parseFloat; + } + + private renderEditInSettingsJson(dataElement: SettingsTreeSettingElement, isSelected: boolean, template: ISettingComplexItemTemplate): void { + template.button.element.tabIndex = isSelected ? 0 : -1; + + template.onChange = () => this._onDidOpenSettings.fire(); } disposeTemplate(tree: ITree, templateId: string, template: IDisposableTemplate): void { @@ -778,6 +964,20 @@ export class SettingsRenderer implements IRenderer { } } +function getDisplayEnumOptions(setting: ISetting): string[] { + if (setting.enum.length > SettingsRenderer.MAX_ENUM_DESCRIPTIONS && setting.enumDescriptions) { + return setting.enum + .map(escapeInvisibleChars) + .map((value, i) => { + return setting.enumDescriptions[i] ? + `${value}: ${setting.enumDescriptions[i]}` : + value; + }); + } + + return setting.enum.map(escapeInvisibleChars); +} + function escapeInvisibleChars(enumValue: string): string { return enumValue && enumValue .replace(/\n/g, '\\n') @@ -842,6 +1042,21 @@ export class SettingsTreeController extends WorkbenchTreeController { ) { super({}, configurationService); } + + protected onLeftClick(tree: ITree, element: any, eventish: IMouseEvent, origin?: string): boolean { + const isLink = eventish.target.tagName.toLowerCase() === 'a' || + eventish.target.parentElement.tagName.toLowerCase() === 'a'; // inside + + if (isLink && DOM.findParentWithClass(eventish.target, 'setting-item-description-markdown', tree.getHTMLElement())) { + return true; + } + + // Without this, clicking on the setting description causes the tree to lose focus. I don't know why. + // The superclass does not always call it because of DND which is not used here. + eventish.preventDefault(); + + return super.onLeftClick(tree, element, eventish, origin); + } } export class SettingsAccessibilityProvider implements IAccessibilityProvider { diff --git a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts index a5adc6ab4b7..f1c464e54c5 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts @@ -23,8 +23,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IEditorInput } from 'vs/workbench/common/editor'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { isEqual } from 'vs/base/common/paths'; import { isLinux } from 'vs/base/common/platform'; +import { isEqual, hasToIgnoreCase } from 'vs/base/common/resources'; const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -66,14 +66,14 @@ export class PreferencesContribution implements IWorkbenchContribution { private onEditorOpening(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions, group: IEditorGroup): IOpenEditorOverride { const resource = editor.getResource(); if ( - !resource || resource.scheme !== 'file' || // require a file path opening - !endsWith(resource.fsPath, 'settings.json') || // file must end in settings.json + !resource || + !endsWith(resource.path, 'settings.json') || // resource must end in settings.json !this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING) // user has not disabled default settings editor ) { return void 0; } - // If the file resource was already opened before in the group, do not prevent + // If the resource was already opened before in the group, do not prevent // the opening of that resource. Otherwise we would have the same settings // opened twice (https://github.com/Microsoft/vscode/issues/36447) if (group.isOpened(editor)) { @@ -81,7 +81,7 @@ export class PreferencesContribution implements IWorkbenchContribution { } // Global User Settings File - if (isEqual(resource.fsPath, this.environmentService.appSettingsPath, !isLinux)) { + if (isEqual(resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) { return { override: this.preferencesService.openGlobalSettings(options, group) }; } @@ -89,7 +89,7 @@ export class PreferencesContribution implements IWorkbenchContribution { const state = this.workspaceService.getWorkbenchState(); if (state === WorkbenchState.FOLDER) { const folders = this.workspaceService.getWorkspace().folders; - if (resource.fsPath === folders[0].toResource(FOLDER_SETTINGS_PATH).fsPath) { + if (isEqual(resource, folders[0].toResource(FOLDER_SETTINGS_PATH), hasToIgnoreCase(resource))) { return { override: this.preferencesService.openWorkspaceSettings(options, group) }; } } @@ -98,7 +98,7 @@ export class PreferencesContribution implements IWorkbenchContribution { else if (state === WorkbenchState.WORKSPACE) { const folders = this.workspaceService.getWorkspace().folders; for (let i = 0; i < folders.length; i++) { - if (resource.fsPath === folders[i].toResource(FOLDER_SETTINGS_PATH).fsPath) { + if (isEqual(resource, folders[i].toResource(FOLDER_SETTINGS_PATH), hasToIgnoreCase(resource))) { return { override: this.preferencesService.openFolderSettings(folders[i].uri, options, group) }; } } diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts index a38ea158de6..b443aabf11b 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts @@ -191,8 +191,8 @@ Registry.as(EditorInputExtensions.EditorInputFactor const category = nls.localize('preferences', "Preferences"); const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenRawDefaultSettingsAction, OpenRawDefaultSettingsAction.ID, OpenRawDefaultSettingsAction.LABEL), 'Preferences: Open Raw Default Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL), 'Preferences: Open Settings (Preview)', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL), 'Preferences: Open Settings', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings (Preview)', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL), 'Preferences: Open User Settings', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsAction, OpenGlobalKeybindingsAction.ID, OpenGlobalKeybindingsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_S) }), 'Preferences: Open Keyboard Shortcuts', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsFileAction, OpenGlobalKeybindingsFileAction.ID, OpenGlobalKeybindingsFileAction.LABEL, { primary: null }), 'Preferences: Open Keyboard Shortcuts File', category); @@ -493,3 +493,23 @@ const focusSettingsListCommand = new FocusSettingsListCommand({ kbOpts: { primary: KeyCode.Enter } }); KeybindingsRegistry.registerCommandAndKeybindingRule(focusSettingsListCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.workbenchContrib())); + +// Preferences menu + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '1_settings', + command: { + id: OpenSettings2Action.ID, + title: nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '2_keybindings', + command: { + id: OpenGlobalKeybindingsAction.ID, + title: nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts index 32464070e4e..0fa751696fc 100644 --- a/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts @@ -5,7 +5,7 @@ 'use strict'; -import 'vs/css!./media/gotoSymbolHandler'; +import 'vs/css!vs/editor/contrib/documentSymbols/media/symbol-icons'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; @@ -449,7 +449,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { // Add results.push(new SymbolEntry(i, - label, icon, description, icon, + label, icon, description, `symbol-icon ${icon}`, element.range, null, this.editorService, this )); } diff --git a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x.svg b/src/vs/workbench/parts/quickopen/browser/media/Constant_16x.svg deleted file mode 100644 index ed2a1751005..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Constant_16x_inverse.svg deleted file mode 100644 index 173e427f964..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_16x.svg b/src/vs/workbench/parts/quickopen/browser/media/EnumItem_16x.svg deleted file mode 100755 index aa901ec1934..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_inverse_16x.svg b/src/vs/workbench/parts/quickopen/browser/media/EnumItem_inverse_16x.svg deleted file mode 100755 index 791759092fc..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_inverse_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode.svg deleted file mode 100644 index 0e202ec10be..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode_inverse.svg deleted file mode 100644 index a508edcd3d6..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode.svg deleted file mode 100644 index ba2f2d091cf..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode_inverse.svg deleted file mode 100644 index 21e1e814b2e..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode.svg deleted file mode 100644 index e776cbc5651..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode_inverse.svg deleted file mode 100644 index 1b76b62be9a..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode.svg deleted file mode 100644 index 788cc8d6450..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode_inverse.svg deleted file mode 100644 index 6cec71cb033..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/gotoSymbolHandler.css b/src/vs/workbench/parts/quickopen/browser/media/gotoSymbolHandler.css deleted file mode 100644 index 4d99facd57b..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/gotoSymbolHandler.css +++ /dev/null @@ -1,154 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constant { - background-image: url('Constant_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constant, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constant { - background-image: url('Constant_16x_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum-member { - background-image: url('EnumItem_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum-member, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum-member { - background-image: url('EnumItem_inverse_16x.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.struct { - background-image: url('Structure_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.struct, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.struct { - background-image: url('Structure_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.event { - background-image: url('Event_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.event, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.event { - background-image: url('Event_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.operator { - background-image: url('Operator_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.operator, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.operator { - background-image: url('Operator_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.type-parameter { - background-image: url('Template_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.type-parameter, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.type-parameter { - background-image: url('Template_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean { - background-image: url('symbol-sprite.svg'); - background-repeat: no-repeat; -} - -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor { background-position: 0 -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable { background-position: -22px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class { background-position: -43px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface { background-position: -63px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module { background-position: -82px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property { background-position: -102px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum { background-position: -122px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string { background-position: -202px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule { background-position: -242px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file { background-position: -262px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array { background-position: -302px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number { background-position: -322px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean { background-position: -343px -4px; } - -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor { background-position: 0 -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable { background-position: -22px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class { background-position: -43px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface { background-position: -63px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module { background-position: -82px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property { background-position: -102px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string { background-position: -202px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum { background-position: -122px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule { background-position: -242px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file { background-position: -262px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array { background-position: -302px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number { background-position: -322px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean { background-position: -342px -24px; } diff --git a/src/vs/workbench/parts/quickopen/browser/media/symbol-sprite.svg b/src/vs/workbench/parts/quickopen/browser/media/symbol-sprite.svg deleted file mode 100644 index ee9a63dcf6f..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/symbol-sprite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts b/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts index be4d6379082..5b8cb97d750 100644 --- a/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts +++ b/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts @@ -9,7 +9,7 @@ import * as env from 'vs/base/common/platform'; import * as nls from 'vs/nls'; import { QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions } from 'vs/workbench/browser/quickopen'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { GotoSymbolAction, GOTO_SYMBOL_PREFIX, SCOPE_PREFIX, GotoSymbolHandler } from 'vs/workbench/parts/quickopen/browser/gotoSymbolHandler'; @@ -144,4 +144,24 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen } ] ) -); \ No newline at end of file +); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '1_open', + command: { + id: ShowAllCommandsAction.ID, + title: nls.localize({ key: 'miCommandPalette', comment: ['&& denotes a mnemonic'] }, "&&Command Palette...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '1_open', + command: { + id: OpenViewPickerAction.ID, + title: nls.localize({ key: 'miOpenView', comment: ['&& denotes a mnemonic'] }, "&&Open View...") + }, + order: 2 +}); diff --git a/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css index e55cb63c612..de4d95ed717 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css @@ -19,6 +19,7 @@ border-top: 4px solid transparent; border-bottom: 4px solid transparent; transition: border-top-width 80ms linear, border-bottom-width 80ms linear, bottom 80ms linear; + pointer-events: none; } .monaco-editor .dirty-diff-glyph:before { diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index e48bf8a0838..108bf404719 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -50,6 +50,7 @@ display: none; } +.scm-viewlet .scm-provider > .type, .scm-viewlet .scm-provider > .name > .type { opacity: 0.7; margin-left: 0.5em; diff --git a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts index 56943a892a0..6a37c940969 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts @@ -13,7 +13,7 @@ import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, To import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { StatusUpdater, StatusBarController } from './scmActivity'; import { SCMViewlet } from 'vs/workbench/parts/scm/electron-browser/scmViewlet'; @@ -88,3 +88,14 @@ Registry.as(ConfigurationExtensions.Configuration).regis } } }); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM") + }, + order: 3 +}); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index b369614adb0..102c393dddb 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -17,7 +17,7 @@ import { PanelViewlet, ViewletPanel, IViewletPanelOptions } from 'vs/workbench/b import { append, $, addClass, toggleClass, trackFocus, Dimension, addDisposableListener } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/parts/scm/common/scm'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -75,7 +75,7 @@ export interface IViewModel { hide(repository: ISCMRepository): void; } -class ProvidersListDelegate implements IDelegate { +class ProvidersListDelegate implements IVirtualDelegate { getHeight(element: ISCMRepository): number { return 22; @@ -190,6 +190,10 @@ class ProviderRenderer implements IRenderer { template.elementDisposable = combinedDisposable(disposables); } + disposeElement(): void { + // noop + } + disposeTemplate(template: ResourceTemplate): void { template.elementDisposable.dispose(); template.dispose(); } } -class ProviderListDelegate implements IDelegate { +class ProviderListDelegate implements IVirtualDelegate { getHeight() { return 22; } @@ -765,19 +777,20 @@ export class RepositoryPanel extends ViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const header = append(container, $('.title.scm-provider')); - const name = append(header, $('.name')); - const title = append(name, $('span.title')); - const type = append(name, $('span.type')); + let title: string; + let type: string; if (this.repository.provider.rootUri) { - title.textContent = basename(this.repository.provider.rootUri.fsPath); - type.textContent = this.repository.provider.label; + title = basename(this.repository.provider.rootUri.fsPath); + type = this.repository.provider.label; } else { - title.textContent = this.repository.provider.label; - type.textContent = ''; + title = this.repository.provider.label; + type = ''; } + super.renderHeaderTitle(container, title); + addClass(container, 'scm-provider'); + append(container, $('span.type', null, type)); const onContextMenu = mapEvent(stop(domEvent(container, 'contextmenu')), e => new StandardMouseEvent(e)); onContextMenu(this.onContextMenu, this, this.disposables); } @@ -1272,7 +1285,8 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle this.addPanels([{ panel, size: panel.minimumSize, index: index++ }]); panel.repository.focus(); panel.onDidFocus(() => this.lastFocusedRepository = panel.repository); - if (newRepositoryPanels.length === 1 || this.lastFocusedRepository === panel.repository) { + + if (this.lastFocusedRepository === panel.repository) { panel.focus(); } }); diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index bd2aa0463a9..b0c4c900a2e 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -84,15 +84,6 @@ export const toggleRegexCommand = (accessor: ServicesAccessor) => { searchView.toggleRegex(); }; -export const FocusActiveEditorCommand = (accessor: ServicesAccessor) => { - const editorService = accessor.get(IEditorService); - const activeControl = editorService.activeControl; - if (activeControl) { - activeControl.focus(); - } - return TPromise.as(true); -}; - export class FocusNextInputAction extends Action { public static readonly ID = 'search.focus.nextInputBox'; @@ -242,7 +233,34 @@ export class CollapseDeepestExpandedLevelAction extends Action { return TPromise.as(null); // Global action disabled if user is in edit mode from another action } - viewer.collapseDeepestExpandedLevel(); + /** + * The hierarchy is FolderMatch, FileMatch, Match. If the top level is FileMatches, then there is only + * one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren, + * i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels. + */ + const navigator = viewer.getNavigator(); + let node = navigator.first(); + let collapseFileMatchLevel = false; + if (node instanceof FolderMatch) { + while (node = navigator.next()) { + if (node instanceof Match) { + collapseFileMatchLevel = true; + break; + } + } + } + + if (collapseFileMatchLevel) { + node = navigator.first(); + do { + if (node instanceof FileMatch) { + viewer.collapse(node); + } + } while (node = navigator.next()); + } else { + viewer.collapseAll(); + } + viewer.clearSelection(); viewer.clearFocus(); viewer.domFocus(); @@ -680,3 +698,11 @@ export const clearHistoryCommand: ICommandHandler = accessor => { const searchHistoryService = accessor.get(ISearchHistoryService); searchHistoryService.clearHistory(); }; + +export const focusSearchListCommand: ICommandHandler = accessor => { + const viewletService = accessor.get(IViewletService); + const panelService = accessor.get(IPanelService); + openSearchView(viewletService, panelService).then(searchView => { + searchView.moveFocusToResults(); + }); +}; diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index d30adb28b21..dd4b0933013 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -237,7 +237,8 @@ export class SearchRenderer extends Disposable implements IRenderer { private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void { if (folderMatch.hasRoot()) { - const fileKind = resources.isEqual(this.contextService.getWorkspaceFolder(folderMatch.resource()).uri, folderMatch.resource()) ? + const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource()); + const fileKind = workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource()) ? FileKind.ROOT_FOLDER : FileKind.FOLDER; diff --git a/src/vs/workbench/parts/search/browser/searchView.ts b/src/vs/workbench/parts/search/browser/searchView.ts index c1af2079000..510fc37cab1 100644 --- a/src/vs/workbench/parts/search/browser/searchView.ts +++ b/src/vs/workbench/parts/search/browser/searchView.ts @@ -732,6 +732,10 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return promise; } + public moveFocusToResults(): void { + this.tree.domFocus(); + } + public focus(): void { super.focus(); @@ -1355,7 +1359,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.setReplaceAllActionState(false); - this.viewModel.search(query).done(onComplete, onError, onProgress); + this.viewModel.search(query, onProgress).done(onComplete, onError); } private updateSearchResultCount(): void { diff --git a/src/vs/workbench/parts/search/common/constants.ts b/src/vs/workbench/parts/search/common/constants.ts index 39abb302748..1f15c344c0e 100644 --- a/src/vs/workbench/parts/search/common/constants.ts +++ b/src/vs/workbench/parts/search/common/constants.ts @@ -16,6 +16,7 @@ export const CopyPathCommandId = 'search.action.copyPath'; export const CopyMatchCommandId = 'search.action.copyMatch'; export const CopyAllCommandId = 'search.action.copyAll'; export const ClearSearchHistoryCommandId = 'search.action.clearHistory'; +export const FocusSearchListCommandID = 'search.action.focusSearchList'; export const ReplaceActionId = 'search.action.replace'; export const ReplaceAllInFileActionId = 'search.action.replaceAllInFile'; export const ReplaceAllInFolderActionId = 'search.action.replaceAllInFolder'; diff --git a/src/vs/workbench/parts/search/common/queryBuilder.ts b/src/vs/workbench/parts/search/common/queryBuilder.ts index 52392ff3903..525753bcae8 100644 --- a/src/vs/workbench/parts/search/common/queryBuilder.ts +++ b/src/vs/workbench/parts/search/common/queryBuilder.ts @@ -75,12 +75,14 @@ export class QueryBuilder { this.resolveSmartCaseToCaseSensitive(contentPattern); } - const query = { + const query: ISearchQuery = { type, folderQueries, usingSearchPaths: !!(searchPaths && searchPaths.length), extraFileResources: options.extraFileResources, - filePattern: options.filePattern, + filePattern: options.filePattern + ? options.filePattern.trim() + : options.filePattern, excludePattern, includePattern, maxResults: options.maxResults, diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index 186d45a23a6..0705774d83c 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -8,7 +8,7 @@ import * as strings from 'vs/base/common/strings'; import * as errors from 'vs/base/common/errors'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { TPromise, PPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { values, ResourceMap, TernarySearchTree } from 'vs/base/common/map'; import { Event, Emitter, fromPromise, stopwatch, anyEvent } from 'vs/base/common/event'; @@ -708,7 +708,7 @@ export class SearchModel extends Disposable { private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); public readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; - private currentRequest: PPromise; + private currentRequest: TPromise; constructor(@ISearchService private searchService: ISearchService, @ITelemetryService private telemetryService: ITelemetryService, @IInstantiationService private instantiationService: IInstantiationService) { super(); @@ -743,18 +743,26 @@ export class SearchModel extends Disposable { return this._searchResult; } - public search(query: ISearchQuery): PPromise { + public search(query: ISearchQuery, onProgress?: (result: ISearchProgressItem) => void): TPromise { this.cancelSearch(); + this._searchQuery = query; - this.currentRequest = this.searchService.search(this._searchQuery); - this.searchResult.clear(); - this._searchResult.query = this._searchQuery; + + const progressEmitter = new Emitter(); this._replacePattern = new ReplacePattern(this._replaceString, this._searchQuery.contentPattern); + this.currentRequest = this.searchService.search(this._searchQuery, p => { + progressEmitter.fire(); + this.onSearchProgress(p); + + if (onProgress) { + onProgress(p); + } + }); + const onDone = fromPromise(this.currentRequest); - const progressEmitter = new Emitter(); const onFirstRender = anyEvent(onDone, progressEmitter.event); const onFirstRenderStopwatch = stopwatch(onFirstRender); /* __GDPR__ @@ -777,12 +785,7 @@ export class SearchModel extends Disposable { const currentRequest = this.currentRequest; this.currentRequest.then( value => this.onSearchCompleted(value, Date.now() - start), - e => this.onSearchError(e, Date.now() - start), - p => { - progressEmitter.fire(); - this.onSearchProgress(p); - } - ); + e => this.onSearchError(e, Date.now() - start)); // this.currentRequest may be completed (and nulled) immediately return currentRequest; diff --git a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts index cfef04d170f..23a3349681d 100644 --- a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts @@ -52,7 +52,7 @@ import { getMultiSelectedResources } from 'vs/workbench/parts/files/browser/file import { Schemas } from 'vs/base/common/network'; import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction, ClearSearchResultsAction, copyPathCommand, copyMatchCommand, copyAllCommand, clearHistoryCommand, FocusNextInputAction, FocusPreviousInputAction, RefreshAction } from 'vs/workbench/parts/search/browser/searchActions'; +import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction, ClearSearchResultsAction, copyPathCommand, copyMatchCommand, copyAllCommand, clearHistoryCommand, FocusNextInputAction, FocusPreviousInputAction, RefreshAction, focusSearchListCommand } from 'vs/workbench/parts/search/browser/searchActions'; import { VIEW_ID, ISearchConfigurationProperties } from 'vs/platform/search/common/search'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -331,6 +331,19 @@ MenuRegistry.appendMenuItem(MenuId.SearchContext, { order: 1 }); +CommandsRegistry.registerCommand({ + id: Constants.FocusSearchListCommandID, + handler: focusSearchListCommand +}); + +const focusSearchListCommandLabel = nls.localize('focusSearchListCommandLabel', "Focus List"); +const FocusSearchListCommand: ICommandAction = { + id: Constants.FocusSearchListCommandID, + title: focusSearchListCommandLabel, + category +}; +MenuRegistry.addCommand(FocusSearchListCommand); + const FIND_IN_FOLDER_ID = 'filesExplorer.findInFolder'; CommandsRegistry.registerCommand({ id: FIND_IN_FOLDER_ID, @@ -531,7 +544,7 @@ configurationRegistry.registerConfiguration({ properties: { 'search.exclude': { type: 'object', - description: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting."), + description: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the [`files.exclude`](#files-exclude) setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), default: { '**/node_modules': true, '**/bower_components': true }, additionalProperties: { anyOf: [ @@ -602,3 +615,14 @@ registerLanguageCommand('_executeWorkspaceSymbolProvider', function (accessor, a } return getWorkspaceSymbols(query); }); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEW_ID, + title: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search") + }, + order: 2 +}); diff --git a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts index c11c6994b96..c3e961993fc 100644 --- a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts @@ -233,6 +233,21 @@ suite('QueryBuilder', () => { }); }); + test('file pattern trimming', () => { + const content = 'content'; + assertEqualQueries( + queryBuilder.text( + PATTERN_INFO, + undefined, + { filePattern: ` ${content} ` } + ), + { + contentPattern: PATTERN_INFO, + filePattern: content, + type: QueryType.Text + }); + }); + test('exclude ./ syntax', () => { assertEqualQueries( queryBuilder.text( diff --git a/src/vs/workbench/parts/search/test/common/searchModel.test.ts b/src/vs/workbench/parts/search/test/common/searchModel.test.ts index ae9d4349c90..589973b27a6 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -6,20 +6,20 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { DeferredPPromise } from 'vs/base/test/common/utils'; -import { PPromise } from 'vs/base/common/winjs.base'; -import { SearchModel } from 'vs/workbench/parts/search/common/searchModel'; +import { timeout } from 'vs/base/common/async'; import URI from 'vs/base/common/uri'; -import { IFileMatch, IFolderQuery, ILineMatch, ISearchService, ISearchComplete, ISearchProgressItem, IUncachedSearchStats } from 'vs/platform/search/common/search'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { DeferredTPromise } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { timeout } from 'vs/base/common/async'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IFileMatch, IFolderQuery, ILineMatch, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, IUncachedSearchStats } from 'vs/platform/search/common/search'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { SearchModel } from 'vs/workbench/parts/search/common/searchModel'; const nullEvent = new class { @@ -68,7 +68,7 @@ suite('SearchModel', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModelService, stubModelService(instantiationService)); instantiationService.stub(ISearchService, {}); - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: [] })); + instantiationService.stub(ISearchService, 'search', TPromise.as({ results: [] })); }); teardown(() => { @@ -77,18 +77,32 @@ suite('SearchModel', () => { }); }); - function ppromiseWithProgress(results: IFileMatch[]): () => PPromise { - return () => new PPromise((resolve, reject, progress) => { - process.nextTick(() => { - results.forEach(progress); - resolve(null); - }); - }); + function searchServiceWithResults(results: IFileMatch[], complete: ISearchComplete = null): ISearchService { + return { + search(query: ISearchQuery, onProgress: (result: ISearchProgressItem) => void): TPromise { + return new TPromise(resolve => { + process.nextTick(() => { + results.forEach(onProgress); + resolve(complete); + }); + }); + } + }; + } + + function searchServiceWithError(error: Error): ISearchService { + return { + search(query: ISearchQuery, onProgress: (result: ISearchProgressItem) => void): TPromise { + return new TPromise((resolve, reject) => { + reject(error); + }); + } + }; } test('Search Model: Search adds to results', async () => { let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); @@ -114,9 +128,9 @@ suite('SearchModel', () => { test('Search Model: Search reports telemetry on search completed', async () => { let target = instantiationService.spy(ITelemetryService, 'publicLog'); let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + instantiationService.stub(ISearchService, searchServiceWithResults(results)); - let testObject = instantiationService.createInstance(SearchModel); + let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(target.calledThrice); @@ -131,7 +145,7 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress([])); + instantiationService.stub(ISearchService, searchServiceWithResults([])); let testObject = instantiationService.createInstance(SearchModel); const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); @@ -150,17 +164,17 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - let promise = new DeferredPPromise(); - instantiationService.stub(ISearchService, 'search', promise); + instantiationService.stub(ISearchService, searchServiceWithResults( + [aRawMatch('file://c:/1', aLineMatch('some preview'))], + { results: [], stats: testSearchStats })); let testObject = instantiationService.createInstance(SearchModel); let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - promise.progress(aRawMatch('file://c:/1', aLineMatch('some preview'))); - promise.complete({ results: [], stats: testSearchStats }); - - return timeout(1).then(() => { - return result.then(() => { + return result.then(() => { + return timeout(1).then(() => { + // timeout because promise handlers may run in a different order. We only care that these + // are fired at some point. assert.ok(target1.calledWith('searchResultsFirstRender')); assert.ok(target1.calledWith('searchResultsFinished')); // assert.equal(1, target2.callCount); @@ -174,14 +188,11 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - let promise = new DeferredPPromise(); - instantiationService.stub(ISearchService, 'search', promise); + instantiationService.stub(ISearchService, searchServiceWithError(new Error('error'))); let testObject = instantiationService.createInstance(SearchModel); let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - promise.error('error'); - return timeout(1).then(() => { return result.then(() => { }, () => { assert.ok(target1.calledWith('searchResultsFirstRender')); @@ -197,7 +208,7 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - let promise = new DeferredPPromise(); + let promise = new DeferredTPromise(); instantiationService.stub(ISearchService, 'search', promise); let testObject = instantiationService.createInstance(SearchModel); @@ -216,12 +227,12 @@ suite('SearchModel', () => { test('Search Model: Search results are cleared during search', async () => { let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(!testObject.searchResult.isEmpty()); - instantiationService.stub(ISearchService, 'search', new DeferredPPromise()); + instantiationService.stub(ISearchService, searchServiceWithResults([])); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(testObject.searchResult.isEmpty()); @@ -229,11 +240,11 @@ suite('SearchModel', () => { test('Search Model: Previous search is cancelled when new search is called', async () => { let target = sinon.spy(); - instantiationService.stub(ISearchService, 'search', new DeferredPPromise((c, e, p) => { }, target)); + instantiationService.stub(ISearchService, 'search', new DeferredTPromise(target)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - instantiationService.stub(ISearchService, 'search', new DeferredPPromise()); + instantiationService.stub(ISearchService, searchServiceWithResults([])); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(target.calledOnce); @@ -241,7 +252,7 @@ suite('SearchModel', () => { test('getReplaceString returns proper replace string for regExpressions', async () => { let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]]))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 're' }, type: 1, folderQueries }); diff --git a/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts b/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts index 71045971e74..c7398954035 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts @@ -217,3 +217,12 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { category: nls.localize('preferences', "Preferences") } }); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '3_snippets', + command: { + id, + title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts index ae5c397ea09..e624664bbe6 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts @@ -21,6 +21,8 @@ export class Snippet { private _codeSnippet: string; private _isBogous: boolean; + readonly prefixLow: string; + constructor( readonly scopes: string[], readonly name: string, @@ -31,6 +33,7 @@ export class Snippet { readonly isFromExtension?: boolean, ) { // + this.prefixLow = prefix.toLowerCase(); } get codeSnippet(): string { diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts index eecdc73b271..69dc72ef055 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts @@ -4,33 +4,33 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { localize } from 'vs/nls'; -import { ITextModel } from 'vs/editor/common/model'; -import { ISuggestSupport, ISuggestResult, ISuggestion, LanguageId, SuggestionType, SnippetType, SuggestContext } from 'vs/editor/common/modes'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { Position } from 'vs/editor/common/core/position'; -import { overlap, compare, startsWith, isFalsyOrWhitespace, endsWith } from 'vs/base/common/strings'; -import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { join, basename, extname } from 'path'; -import * as resources from 'vs/base/common/resources'; -import { mkdirp, readdir, exists } from 'vs/base/node/pfs'; -import { watch } from 'vs/base/node/extfs'; -import { SnippetFile, Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; -import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; +import { basename, extname, join } from 'path'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { values } from 'vs/base/common/map'; +import * as resources from 'vs/base/common/resources'; +import { compare, endsWith, isFalsyOrWhitespace } from 'vs/base/common/strings'; +import URI from 'vs/base/common/uri'; +import { watch } from 'vs/base/node/extfs'; +import { exists, mkdirp, readdir } from 'vs/base/node/pfs'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { ISuggestion, ISuggestResult, ISuggestSupport, LanguageId, SnippetType, SuggestContext, SuggestionType } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { values } from 'vs/base/common/map'; -import URI from 'vs/base/common/uri'; -import { IFileService } from 'vs/platform/files/common/files'; +import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; +import { Snippet, SnippetFile } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; namespace schema { @@ -253,14 +253,12 @@ class SnippetsService implements ISnippetsService { } }); }, (error: string) => this._logService.error(error)); - this._disposables.push({ - dispose: () => { - if (watcher) { - watcher.removeAllListeners(); - watcher.close(); - } + this._disposables.push(toDisposable(() => { + if (watcher) { + watcher.removeAllListeners(); + watcher.close(); } - }); + })); }).then(undefined, err => { this._logService.error('Failed to load user snippets', err); @@ -326,35 +324,46 @@ export class SnippetSuggestProvider implements ISuggestSupport { const languageId = this._getLanguageIdAtPosition(model, position); return this._snippets.getSnippets(languageId).then(snippets => { - const suggestions: SnippetSuggestion[] = []; + let suggestions: SnippetSuggestion[]; + let pos = { lineNumber: position.lineNumber, column: Math.max(1, position.column - 100) }; + let lineOffsets: number[] = []; + let linePrefixLow = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase(); - const lowWordUntil = model.getWordUntilPosition(position).word.toLowerCase(); - const lowLineUntil = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase(); + while (pos.column < position.column) { + let word = model.getWordAtPosition(pos); + if (word) { + // at a word + lineOffsets.push(word.startColumn - 1); + pos.column = word.endColumn + 1; - for (const snippet of snippets) { + if (word.endColumn - 1 < linePrefixLow.length && !/\s/.test(linePrefixLow[word.endColumn - 1])) { + lineOffsets.push(word.endColumn - 1); + } - const lowPrefix = snippet.prefix.toLowerCase(); - let overwriteBefore = 0; - let accetSnippet = true; - - if (typeof context.triggerCharacter === 'string') { - // cheap match on the trigger-character - overwriteBefore = context.triggerCharacter.length; - accetSnippet = startsWith(lowPrefix, context.triggerCharacter.toLowerCase()); - - } else if (lowWordUntil.length > 0 && startsWith(lowPrefix, lowWordUntil)) { - // cheap match on the (none-empty) current word - overwriteBefore = lowWordUntil.length; - accetSnippet = true; - - } else if (lowLineUntil.length > 0 && lowLineUntil.match(/[^\s]$/)) { - // compute overlap between snippet and (none-empty) line on text - overwriteBefore = overlap(lowLineUntil, snippet.prefix.toLowerCase()); - accetSnippet = overwriteBefore > 0 && !model.getWordAtPosition(new Position(position.lineNumber, position.column - overwriteBefore)); + } else if (!/\s/.test(linePrefixLow[pos.column - 1])) { + // at a none-whitespace character + lineOffsets.push(pos.column - 1); + pos.column += 1; + } else { + // always advance! + pos.column += 1; } + } - if (accetSnippet) { - suggestions.push(new SnippetSuggestion(snippet, overwriteBefore)); + if (lineOffsets.length === 0) { + // no interesting spans found -> pick all snippets + suggestions = snippets.map(snippet => new SnippetSuggestion(snippet, 0)); + + } else { + let consumed = new Set(); + suggestions = []; + for (const start of lineOffsets) { + for (const snippet of snippets) { + if (!consumed.has(snippet) && matches(linePrefixLow, start, snippet.prefixLow, 0)) { + suggestions.push(new SnippetSuggestion(snippet, linePrefixLow.length - start)); + consumed.add(snippet); + } + } } } @@ -394,6 +403,16 @@ export class SnippetSuggestProvider implements ISuggestSupport { } } +function matches(pattern: string, patternStart: number, word: string, wordStart: number): boolean { + while (patternStart < pattern.length && wordStart < word.length) { + if (pattern[patternStart] === word[wordStart]) { + patternStart += 1; + } + wordStart += 1; + } + return patternStart === pattern.length; +} + export function getNonWhitespacePrefix(model: ISimpleModel, position: Position): string { /** * Do not analyze more characters diff --git a/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts b/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts index 6c469038ba7..70f0ab60b99 100644 --- a/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts +++ b/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts @@ -83,10 +83,63 @@ suite('SnippetsService', function () { assert.equal(result.incomplete, undefined); assert.equal(result.suggestions.length, 1); assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].overwriteBefore, 3); assert.equal(result.suggestions[0].insertText, 'barCodeSnippet'); }); }); + test('snippet completions - with different prefixes', async function () { + + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'barTest', + 'bar', + '', + 's1', + '' + ), new Snippet( + ['fooLang'], + 'name', + 'bar-bar', + '', + 's2', + '' + )]); + + const provider = new SnippetSuggestProvider(modeService, snippetService); + const model = TextModel.createFromString('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang')); + + await provider.provideCompletionItems(model, new Position(1, 3), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 2); + assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].insertText, 's1'); + assert.equal(result.suggestions[0].overwriteBefore, 2); + assert.equal(result.suggestions[1].label, 'bar-bar'); + assert.equal(result.suggestions[1].insertText, 's2'); + assert.equal(result.suggestions[1].overwriteBefore, 2); + }); + + await provider.provideCompletionItems(model, new Position(1, 5), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].label, 'bar-bar'); + assert.equal(result.suggestions[0].insertText, 's2'); + assert.equal(result.suggestions[0].overwriteBefore, 4); + }); + + await provider.provideCompletionItems(model, new Position(1, 6), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 2); + assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].insertText, 's1'); + assert.equal(result.suggestions[0].overwriteBefore, 1); + assert.equal(result.suggestions[1].label, 'bar-bar'); + assert.equal(result.suggestions[1].insertText, 's2'); + assert.equal(result.suggestions[1].overwriteBefore, 5); + }); + }); + test('Cannot use " { assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].overwriteBefore, 2); model.dispose(); model = TextModel.createFromString('a { - - assert.equal(result.suggestions.length, 0); + assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].overwriteBefore, 2); model.dispose(); }); }); @@ -170,4 +224,27 @@ suite('SnippetsService', function () { assert.equal(second.label, 'second'); }); }); + + test('Dash in snippets prefix broken #53945', async function () { + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'p-a', + 'p-a', + '', + 'second', + '' + )]); + const provider = new SnippetSuggestProvider(modeService, snippetService); + + let model = TextModel.createFromString('p-', undefined, modeService.getLanguageIdentifier('fooLang')); + + let result = await provider.provideCompletionItems(model, new Position(1, 2), suggestContext); + assert.equal(result.suggestions.length, 1); + + result = await provider.provideCompletionItems(model, new Position(1, 3), suggestContext); + assert.equal(result.suggestions.length, 1); + + result = await provider.provideCompletionItems(model, new Position(1, 3), { triggerCharacter: '-', triggerKind: SuggestTriggerKind.TriggerCharacter }); + assert.equal(result.suggestions.length, 1); + }); }); diff --git a/src/vs/workbench/parts/stats/node/workspaceStats.ts b/src/vs/workbench/parts/stats/node/workspaceStats.ts index 60b4979fd47..19bc5120d59 100644 --- a/src/vs/workbench/parts/stats/node/workspaceStats.ts +++ b/src/vs/workbench/parts/stats/node/workspaceStats.ts @@ -16,6 +16,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWindowConfiguration, IWindowService } from 'vs/platform/windows/common/windows'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { endsWith } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/; const SshUrlMatcher = /^([^@:]+@)?([^:]+):(.+)$/; @@ -240,7 +241,8 @@ export class WorkspaceStats implements IWorkbenchContribution { workspaceId = void 0; break; case WorkbenchState.FOLDER: - workspaceId = crypto.createHash('sha1').update(workspace.folders[0].uri.fsPath).digest('hex'); + // TODO: #54483 @Ben + workspaceId = crypto.createHash('sha1').update(workspace.folders[0].uri.scheme === Schemas.file ? workspace.folders[0].uri.fsPath : workspace.folders[0].uri.toString()).digest('hex'); break; case WorkbenchState.WORKSPACE: workspaceId = crypto.createHash('sha1').update(workspace.configuration.fsPath).digest('hex'); 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/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index 7d92c1af7da..9a771032c99 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -12,6 +12,9 @@ import { UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ProblemMatcher } from 'vs/workbench/parts/tasks/common/problemMatcher'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const TASK_RUNNING_STATE = new RawContextKey('taskRunning', false); export enum ShellQuoting { /** 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 70ebf3100a0..f301f5471da 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -17,7 +17,7 @@ import URI from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { Action } from 'vs/base/common/actions'; import * as Dom from 'vs/base/browser/dom'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as Types from 'vs/base/common/types'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -31,7 +31,7 @@ import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -75,7 +75,7 @@ import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, T import { Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, TaskEvent, TaskEventKind, TaskSet, TaskGroup, GroupType, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, - TaskSorter, TaskIdentifier, KeyedTaskIdentifier + TaskSorter, TaskIdentifier, KeyedTaskIdentifier, TASK_RUNNING_STATE } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, ITaskProvider, RunOptions, CustomizationProperties, TaskFilter } from 'vs/workbench/parts/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; @@ -253,11 +253,9 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { this.updateStyles(); - return { - dispose: () => { - callOnDispose = dispose(callOnDispose); - } - }; + return toDisposable(() => { + callOnDispose = dispose(callOnDispose); + }); } private ignoreEvent(event: TaskEvent): boolean { @@ -459,6 +457,8 @@ class TaskService implements ITaskService { private _taskSystemListener: IDisposable; private _recentlyUsedTasks: LinkedMap; + private _taskRunningState: IContextKey; + private _outputChannel: IOutputChannel; private readonly _onDidStateChange: Emitter; @@ -482,7 +482,9 @@ class TaskService implements ITaskService { @IOpenerService private openerService: IOpenerService, @IWindowService private readonly _windowService: IWindowService, @IDialogService private dialogService: IDialogService, - @INotificationService private notificationService: INotificationService + @INotificationService private notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { this._configHasErrors = false; this._workspaceTasksPromise = undefined; @@ -521,6 +523,7 @@ class TaskService implements ITaskService { this.updateSetup(folderSetup); this.updateWorkspaceTasks(); }); + this._taskRunningState = TASK_RUNNING_STATE.bindTo(contextKeyService); lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown())); this._onDidStateChange = new Emitter(); this.registerCommands(); @@ -1292,13 +1295,18 @@ class TaskService implements ITaskService { this._taskSystem = system; } this._taskSystemListener = this._taskSystem.onDidStateChange((event) => { + if (this._taskSystem) { + this._taskRunningState.set(this._taskSystem.isActiveSync()); + } this._onDidStateChange.fire(event); }); return this._taskSystem; } 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; @@ -1327,7 +1335,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); @@ -2418,6 +2426,75 @@ class TaskService implements ITaskService { } } +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '1_run', + command: { + id: 'workbench.action.tasks.runTask', + title: nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '1_run', + command: { + id: 'workbench.action.tasks.build', + title: nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task...") + }, + order: 2 +}); + +// Manage Tasks +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '2_manage', + command: { + precondition: TASK_RUNNING_STATE, + id: 'workbench.action.tasks.showTasks', + title: nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '2_manage', + command: { + precondition: TASK_RUNNING_STATE, + id: 'workbench.action.tasks.restartTask', + title: nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '2_manage', + command: { + precondition: TASK_RUNNING_STATE, + id: 'workbench.action.tasks.terminate', + title: nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task...") + }, + order: 3 +}); + +// Configure Tasks +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '3_configure', + command: { + id: 'workbench.action.tasks.configureTaskRunner', + title: nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '3_configure', + command: { + id: 'workbench.action.tasks.configureDefaultBuildTask', + title: nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task...") + }, + order: 2 +}); + + MenuRegistry.addCommand({ id: ConfigureTaskAction.ID, title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.showLog', title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.runTask', title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, category: { value: tasksCategory, original: 'Tasks' } }); @@ -2485,6 +2562,8 @@ let schema: IJSONSchema = { import schemaVersion1 from './jsonSchema_v1'; import schemaVersion2 from './jsonSchema_v2'; +import { TaskDefinitionRegistry } from 'vs/workbench/parts/tasks/common/taskDefinitionRegistry'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; schema.definitions = { ...schemaVersion1.definitions, ...schemaVersion2.definitions, diff --git a/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts b/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts index 8169d1f3c8c..8cec848aedb 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts @@ -12,7 +12,7 @@ import * as Strings from 'vs/base/common/strings'; import * as Collections from 'vs/base/common/collections'; import { CommandOptions, Source, ErrorData } from 'vs/base/common/processes'; -import { LineProcess } from 'vs/base/node/processes'; +import { LineProcess, LineData } from 'vs/base/node/processes'; import { IFileService } from 'vs/platform/files/common/files'; @@ -269,7 +269,20 @@ export class ProcessRunnerDetector { private runDetection(process: LineProcess, command: string, isShellCommand: boolean, matcher: TaskDetectorMatcher, problemMatchers: string[], list: boolean): TPromise { let tasks: string[] = []; matcher.init(); - return process.start().then((success) => { + + const onProgress = (progress: LineData) => { + if (progress.source === Source.stderr) { + this._stderr.push(progress.line); + return; + } + let line = Strings.removeAnsiEscapeCodes(progress.line); + let matches = matcher.match(tasks, line); + if (matches && matches.length > 0) { + tasks.push(matches[1]); + } + }; + + return process.start(onProgress).then((success) => { if (tasks.length === 0) { if (success.cmdCode !== 0) { if (command === 'gulp') { @@ -305,16 +318,6 @@ export class ProcessRunnerDetector { this._stderr.push(nls.localize('TaskSystemDetector.noProgram', 'Program {0} was not found. Message is {1}', command, error.message)); } return { config: null, stdout: this._stdout, stderr: this._stderr }; - }, (progress) => { - if (progress.source === Source.stderr) { - this._stderr.push(progress.line); - return; - } - let line = Strings.removeAnsiEscapeCodes(progress.line); - let matches = matcher.match(tasks, line); - if (matches && matches.length > 0) { - tasks.push(matches[1]); - } }); } diff --git a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts index 8ec0759e9e1..810cfd8615b 100644 --- a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts @@ -249,7 +249,21 @@ export class ProcessTaskSystem implements ITaskSystem { this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); let processStartedSignaled: boolean = false; - const startPromise = this.childProcess.start(); + const onProgress = (progress: LineData) => { + let line = Strings.removeAnsiEscapeCodes(progress.line); + this.outputChannel.append(line + '\n'); + watchingProblemMatcher.processLine(line); + if (delayer === null) { + delayer = new Async.Delayer(3000); + } + delayer.trigger(() => { + watchingProblemMatcher.forceDelivery(); + return null; + }).then(() => { + delayer = null; + }); + }; + const startPromise = this.childProcess.start(onProgress); this.childProcess.pid.then(pid => { if (pid !== -1) { processStartedSignaled = true; @@ -287,19 +301,6 @@ export class ProcessTaskSystem implements ITaskSystem { } eventCounter = 0; return this.handleError(task, error); - }, (progress: LineData) => { - let line = Strings.removeAnsiEscapeCodes(progress.line); - this.outputChannel.append(line + '\n'); - watchingProblemMatcher.processLine(line); - if (delayer === null) { - delayer = new Async.Delayer(3000); - } - delayer.trigger(() => { - watchingProblemMatcher.forceDelivery(); - return null; - }).then(() => { - delayer = null; - }); }); let result: ITaskExecuteResult = (task).tscWatch ? { kind: TaskExecuteKind.Started, started: { restartOnFileChanges: '**/*.ts' }, promise: this.activeTaskPromise } @@ -312,7 +313,12 @@ export class ProcessTaskSystem implements ITaskSystem { this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); let processStartedSignaled: boolean = false; - const startPromise = this.childProcess.start(); + const onProgress = (progress) => { + let line = Strings.removeAnsiEscapeCodes(progress.line); + this.outputChannel.append(line + '\n'); + startStopProblemMatcher.processLine(line); + }; + const startPromise = this.childProcess.start(onProgress); this.childProcess.pid.then(pid => { if (pid !== -1) { processStartedSignaled = true; @@ -340,10 +346,6 @@ export class ProcessTaskSystem implements ITaskSystem { this._onDidStateChange.fire(inactiveEvent); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); return this.handleError(task, error); - }, (progress) => { - let line = Strings.removeAnsiEscapeCodes(progress.line); - this.outputChannel.append(line + '\n'); - startStopProblemMatcher.processLine(line); }); return { kind: TaskExecuteKind.Started, started: {}, promise: this.activeTaskPromise }; } diff --git a/src/vs/workbench/parts/tasks/node/tasks.ts b/src/vs/workbench/parts/tasks/node/tasks.ts index c0cd15ad06d..a24e94c013a 100644 --- a/src/vs/workbench/parts/tasks/node/tasks.ts +++ b/src/vs/workbench/parts/tasks/node/tasks.ts @@ -27,7 +27,10 @@ namespace TaskDefinition { export function createTaskIdentifier(external: TaskIdentifier, reporter: { error(message: string): void; }): KeyedTaskIdentifier | undefined { let definition = TaskDefinitionRegistry.get(external.type); if (definition === void 0) { - return undefined; + // We have no task definition so we can't sanitize the literal. Take it as is + let copy = Objects.deepClone(external); + delete copy._key; + return KeyedTaskIdentifier.create(copy); } let literal: { type: string;[name: string]: any } = Object.create(null); diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 81613ecef7b..3e637c51d8f 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -14,9 +14,11 @@ export const TERMINAL_PANEL_ID = 'workbench.panel.terminal'; export const TERMINAL_SERVICE_ID = 'terminalService'; -/** A context key that is set when the integrated terminal has focus. */ +/** A context key that is set when there is at least one opened integrated terminal. */ +export const KEYBINDING_CONTEXT_TERMINAL_IS_OPEN = new RawContextKey('terminalIsOpen', false); +/** A context key that is set when the integrated terminal has focus. */ export const KEYBINDING_CONTEXT_TERMINAL_FOCUS = new RawContextKey('terminalFocus', undefined); -/** A context key that is set when the integrated terminal does not have focus. */ +/** A context key that is set when the integrated terminal does not have focus. */ export const KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FOCUS.toNegated(); /** A keybinding context key that is set when the integrated terminal has text selected. */ @@ -596,9 +598,9 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { emitPid(pid: number): void; emitExit(exitCode: number): void; - onInput(listener: (data: string) => void): void; - onResize(listener: (cols: number, rows: number) => void): void; - onShutdown(listener: () => void): void; + onInput: Event; + onResize: Event<{ cols: number, rows: number }>; + onShutdown: Event; } export interface ITerminalProcessExtHostRequest { diff --git a/src/vs/workbench/parts/terminal/common/terminalCommands.ts b/src/vs/workbench/parts/terminal/common/terminalCommands.ts index 828588d03b1..fa7471a2316 100644 --- a/src/vs/workbench/parts/terminal/common/terminalCommands.ts +++ b/src/vs/workbench/parts/terminal/common/terminalCommands.ts @@ -6,7 +6,58 @@ import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; -export function setup(): void { +export const enum TERMINAL_COMMAND_ID { + TOGGLE = 'workbench.action.terminal.toggleTerminal', + KILL = 'workbench.action.terminal.kill', + QUICK_KILL = 'workbench.action.terminal.quickKill', + COPY_SELECTION = 'workbench.action.terminal.copySelection', + SELECT_ALL = 'workbench.action.terminal.selectAll', + DELETE_WORD_LEFT = 'workbench.action.terminal.deleteWordLeft', + DELETE_WORD_RIGHT = 'workbench.action.terminal.deleteWordRight', + MOVE_TO_LINE_START = 'workbench.action.terminal.moveToLineStart', + MOVE_TO_LINE_END = 'workbench.action.terminal.moveToLineEnd', + NEW = 'workbench.action.terminal.new', + NEW_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.newInActiveWorkspace', + SPLIT = 'workbench.action.terminal.split', + SPLIT_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.splitInActiveWorkspace', + FOCUS_PREVIOUS_PANE = 'workbench.action.terminal.focusPreviousPane', + FOCUS_NEXT_PANE = 'workbench.action.terminal.focusNextPane', + RESIZE_PANE_LEFT = 'workbench.action.terminal.resizePaneLeft', + RESIZE_PANE_RIGHT = 'workbench.action.terminal.resizePaneRight', + RESIZE_PANE_UP = 'workbench.action.terminal.resizePaneUp', + RESIZE_PANE_DOWN = 'workbench.action.terminal.resizePaneDown', + FOCUS = 'workbench.action.terminal.focus', + FOCUS_NEXT = 'workbench.action.terminal.focusNext', + FOCUS_PREVIOUS = 'workbench.action.terminal.focusPrevious', + PASTE = 'workbench.action.terminal.paste', + SELECT_DEFAULT_SHELL = 'workbench.action.terminal.selectDefaultShell', + RUN_SELECTED_TEXT = 'workbench.action.terminal.runSelectedText', + RUN_ACTIVE_FILE = 'workbench.action.terminal.runActiveFile', + SWITCH_TERMINAL = 'workbench.action.terminal.switchTerminal', + SCROLL_DOWN_LINE = 'workbench.action.terminal.scrollDown', + SCROLL_DOWN_PAGE = 'workbench.action.terminal.scrollDownPage', + SCROLL_TO_BOTTOM = 'workbench.action.terminal.scrollToBottom', + SCROLL_UP_LINE = 'workbench.action.terminal.scrollUp', + SCROLL_UP_PAGE = 'workbench.action.terminal.scrollUpPage', + SCROLL_TO_TOP = 'workbench.action.terminal.scrollToTop', + CLEAR = 'workbench.action.terminal.clear', + CLEAR_SELECTION = 'workbench.action.terminal.clearSelection', + WORKSPACE_SHELL_ALLOW = 'workbench.action.terminal.allowWorkspaceShell', + WORKSPACE_SHELL_DISALLOW = 'workbench.action.terminal.disallowWorkspaceShell', + RENAME = 'workbench.action.terminal.rename', + FIND_WIDGET_FOCUS = 'workbench.action.terminal.focusFindWidget', + FIND_WIDGET_HIDE = 'workbench.action.terminal.hideFindWidget', + QUICK_OPEN_TERM = 'workbench.action.quickOpenTerm', + SCROLL_TO_PREVIOUS_COMMAND = 'workbench.action.terminal.scrollToPreviousCommand', + SCROLL_TO_NEXT_COMMAND = 'workbench.action.terminal.scrollToNextCommand', + SELECT_TO_PREVIOUS_COMMAND = 'workbench.action.terminal.selectToPreviousCommand', + SELECT_TO_NEXT_COMMAND = 'workbench.action.terminal.selectToNextCommand', + SELECT_TO_PREVIOUS_LINE = 'workbench.action.terminal.selectToPreviousLine', + SELECT_TO_NEXT_LINE = 'workbench.action.terminal.selectToNextLine', +} + + +export function setupTerminalCommands(): void { registerOpenTerminalAtIndexCommands(); } diff --git a/src/vs/workbench/parts/terminal/common/terminalMenu.ts b/src/vs/workbench/parts/terminal/common/terminalMenu.ts new file mode 100644 index 00000000000..52498a02f51 --- /dev/null +++ b/src/vs/workbench/parts/terminal/common/terminalMenu.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; + +export function setupTerminalMenu() { + + // View menu + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: TERMINAL_COMMAND_ID.TOGGLE, + title: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Integrated Terminal") + }, + order: 3 + }); + + // Manage + const manageGroup = '1_manage'; + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: manageGroup, + command: { + id: TERMINAL_COMMAND_ID.NEW, + title: nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal") + }, + order: 1 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: manageGroup, + command: { + id: TERMINAL_COMMAND_ID.SPLIT, + title: nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: manageGroup, + command: { + id: TERMINAL_COMMAND_ID.KILL, + title: nls.localize({ key: 'miKillTerminal', comment: ['&& denotes a mnemonic'] }, "&&Kill Terminal"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 3 + }); + + // Run + const runGroup = '2_run'; + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: runGroup, + command: { + id: TERMINAL_COMMAND_ID.CLEAR, + title: nls.localize({ key: 'miClear', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 1 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: runGroup, + command: { + id: TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, + title: nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File") + }, + order: 2 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: runGroup, + command: { + id: TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, + title: nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text") + }, + order: 3 + }); + + // Navigation + const navigationGroup = '3_navigation'; + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: navigationGroup, + command: { + id: TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND, + title: nls.localize({ key: 'miScrollToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Previous Command"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 1 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: navigationGroup, + command: { + id: TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND, + title: nls.localize({ key: 'miScrollToNextCommand', comment: ['&& denotes a mnemonic'] }, "Scroll To Next Command"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 2 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: navigationGroup, + command: { + id: TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND, + title: nls.localize({ key: 'miSelectToPreviousCommand', comment: ['&& denotes a mnemonic'] }, "Select To Previous Command"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 3 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: navigationGroup, + command: { + id: TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND, + title: nls.localize({ key: 'miSelectToNextCommand', comment: ['&& denotes a mnemonic'] }, "Select To Next Command"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 4 + }); +} diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index 20383cf4d66..2c28f173a59 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -9,7 +9,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -71,6 +71,18 @@ export abstract class TerminalService implements ITerminalService { this.onTabDisposed(tab => this._removeTab(tab)); lifecycleService.when(LifecyclePhase.Restoring).then(() => this._restoreTabs()); + + this._handleContextKeys(); + } + + private _handleContextKeys(): void { + const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); + + const updateTerminalContextKeys = () => { + terminalIsOpenContext.set(this.terminalInstances.length > 0); + }; + + this.onInstancesChanged(() => updateTerminalContextKeys()); } protected abstract _showTerminalCloseConfirmation(): TPromise; diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css index 42d4a6496db..c5da665535e 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css @@ -17,6 +17,7 @@ height: 100%; width: 100%; box-sizing: border-box; + overflow: hidden; } .monaco-workbench .panel.integrated-terminal .terminal-tab { 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 97c1069a8c5..74c5d15af9b 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -11,7 +11,6 @@ import * as debugActions from 'vs/workbench/parts/debug/browser/debugActions'; import * as nls from 'vs/nls'; import * as panel from 'vs/workbench/browser/panel'; import * as platform from 'vs/base/common/platform'; -import * as terminalCommands from 'vs/workbench/parts/terminal/common/terminalCommands'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, DEFAULT_LINE_HEIGHT, DEFAULT_LETTER_SPACING } from 'vs/workbench/parts/terminal/common/terminal'; import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal'; @@ -37,6 +36,8 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; import { TerminalPickerHandler } from 'vs/workbench/parts/terminal/browser/terminalQuickOpen'; +import { setupTerminalCommands, TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; +import { setupTerminalMenu } from 'vs/workbench/parts/terminal/common/terminalMenu'; const quickOpenRegistry = (Registry.as(QuickOpenExtensions.Quickopen)); @@ -68,76 +69,76 @@ actionBarRegistry.registerActionBarContributor(Scope.VIEWER, QuickOpenActionTerm const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ - 'id': 'terminal', - 'order': 100, - 'title': nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), - 'type': 'object', - 'properties': { + id: 'terminal', + order: 100, + title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), + type: 'object', + properties: { 'terminal.integrated.shell.linux': { - 'description': nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux."), - 'type': 'string', - 'default': getTerminalDefaultShellUnixLike() + description: nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux."), + type: 'string', + default: getTerminalDefaultShellUnixLike() }, 'terminal.integrated.shellArgs.linux': { - 'description': nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal."), - 'type': 'array', - 'items': { - 'type': 'string' + description: nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal."), + type: 'array', + items: { + type: 'string' }, - 'default': [] + default: [] }, 'terminal.integrated.shell.osx': { - 'description': nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on OS X."), - 'type': 'string', - 'default': getTerminalDefaultShellUnixLike() + description: nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS."), + type: 'string', + default: getTerminalDefaultShellUnixLike() }, 'terminal.integrated.shellArgs.osx': { - 'description': nls.localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the OS X terminal."), - 'type': 'array', - 'items': { - 'type': 'string' + description: nls.localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal."), + type: 'array', + items: { + type: 'string' }, // Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This // is the reason terminals on macOS typically run login shells by default which set up // the environment. See http://unix.stackexchange.com/a/119675/115410 - 'default': ['-l'] + default: ['-l'] }, 'terminal.integrated.shell.windows': { - 'description': nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. When using shells shipped with Windows (cmd, PowerShell or Bash on Ubuntu)."), - 'type': 'string', - 'default': getTerminalDefaultShellWindows() + description: nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. When using shells shipped with Windows (cmd, PowerShell or Bash on Ubuntu)."), + type: 'string', + default: getTerminalDefaultShellWindows() }, 'terminal.integrated.shellArgs.windows': { - 'description': nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal."), - 'type': 'array', - 'items': { - 'type': 'string' + description: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal."), + type: 'array', + items: { + type: 'string' }, - 'default': [] + default: [] }, 'terminal.integrated.macOptionIsMeta': { - 'description': nls.localize('terminal.integrated.macOptionIsMeta', "Treat the option key as the meta key in the terminal on macOS."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.macOptionIsMeta', "Treat the option key as the meta key in the terminal on macOS."), + 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 + description: nls.localize('terminal.integrated.macOptionClickForcesSelection', "Whether to force selection 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, for example, when mouse mode is enabled in tmux."), + 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', - 'default': false + description: nls.localize('terminal.integrated.copyOnSelection', "When set, text selected in the terminal will be copied to the clipboard."), + type: 'boolean', + default: false }, 'terminal.integrated.drawBoldTextInBrightColors': { - 'description': nls.localize('terminal.integrated.drawBoldTextInBrightColors', "When set, bold text in the terminal will always use the \"bright\" ANSI color variant."), - 'type': 'boolean', - 'default': true + description: nls.localize('terminal.integrated.drawBoldTextInBrightColors', "When set, bold text in the terminal will always use the \"bright\" ANSI color variant."), + type: 'boolean', + default: true }, 'terminal.integrated.fontFamily': { - 'description': nls.localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to editor.fontFamily's value."), - 'type': 'string' + description: nls.localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."), + type: 'string' }, // TODO: Support font ligatures // 'terminal.integrated.fontLigatures': { @@ -146,97 +147,140 @@ configurationRegistry.registerConfiguration({ // 'default': false // }, 'terminal.integrated.fontSize': { - 'description': nls.localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."), - 'type': 'number', - 'default': EDITOR_FONT_DEFAULTS.fontSize + description: nls.localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."), + type: 'number', + default: EDITOR_FONT_DEFAULTS.fontSize }, 'terminal.integrated.letterSpacing': { - 'description': nls.localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."), - 'type': 'number', - 'default': DEFAULT_LETTER_SPACING + description: nls.localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."), + type: 'number', + default: DEFAULT_LETTER_SPACING }, 'terminal.integrated.lineHeight': { - 'description': nls.localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."), - 'type': 'number', - 'default': DEFAULT_LINE_HEIGHT + description: nls.localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."), + type: 'number', + default: DEFAULT_LINE_HEIGHT }, 'terminal.integrated.fontWeight': { - 'type': 'string', - 'enum': ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - 'description': nls.localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."), - 'default': 'normal' + type: 'string', + enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + description: nls.localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."), + default: 'normal' }, 'terminal.integrated.fontWeightBold': { - 'type': 'string', - 'enum': ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - 'description': nls.localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."), - 'default': 'bold' + type: 'string', + enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + description: nls.localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."), + default: 'bold' }, 'terminal.integrated.cursorBlinking': { - 'description': nls.localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."), + type: 'boolean', + default: false }, 'terminal.integrated.cursorStyle': { - 'description': nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."), - 'enum': [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE], - 'default': TerminalCursorStyle.BLOCK + description: nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."), + enum: [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE], + default: TerminalCursorStyle.BLOCK }, 'terminal.integrated.scrollback': { - 'description': nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."), - 'type': 'number', - 'default': 1000 + description: nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."), + type: 'number', + default: 1000 }, 'terminal.integrated.setLocaleVariables': { - 'description': nls.localize('terminal.integrated.setLocaleVariables', "Controls whether locale variables are set at startup of the terminal, this defaults to true on OS X, false on other platforms."), - 'type': 'boolean', - 'default': platform.isMacintosh + description: nls.localize('terminal.integrated.setLocaleVariables', "Controls whether locale variables are set at startup of the terminal, this defaults to `true` on macOS, `false` on other platforms."), + type: 'boolean', + default: platform.isMacintosh }, 'terminal.integrated.rendererType': { - 'type': 'string', - 'enum': ['auto', 'canvas', 'dom'], + type: 'string', + enum: ['auto', 'canvas', 'dom'], + enumDescriptions: [ + nls.localize('terminal.integrated.rendererType.auto', "Let VS Code guess which renderer to use."), + nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer"), + nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer.") + ], default: 'auto', - description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered, the options are \"canvas\" for the standard (fast) canvas renderer, \"dom\" for the fallback DOM-based renderer or \"auto\" which lets VS Code guess which will be best. This setting needs VS Code to reload in order to take effect.") + description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered. This setting needs VS Code to reload in order to take effect.") }, 'terminal.integrated.rightClickBehavior': { - 'type': 'string', - 'enum': ['default', 'copyPaste', 'selectWord'], + type: 'string', + enum: ['default', 'copyPaste', 'selectWord'], + enumDescriptions: [ + nls.localize('terminal.integrated.rightClickBehavior.default', "Show the context menu."), + nls.localize('terminal.integrated.rightClickBehavior.copyPaste', "Copy when there is a selection, otherwise paste."), + nls.localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu.") + ], default: platform.isMacintosh ? 'selectWord' : platform.isWindows ? 'copyPaste' : 'default', - description: nls.localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click, possibilities are \"default\", \"copyPaste\", and \"selectWord\". \"default\" will show the context menu, \"copyPaste\" will copy when there is a selection otherwise paste, \"selectWord\" will select the word under the cursor and show the context menu.") + description: nls.localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click.") }, 'terminal.integrated.cwd': { - 'description': nls.localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."), - 'type': 'string', - 'default': undefined + description: nls.localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."), + type: 'string', + default: undefined }, 'terminal.integrated.confirmOnExit': { - 'description': nls.localize('terminal.integrated.confirmOnExit', "Whether to confirm on exit if there are active terminal sessions."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.confirmOnExit', "Whether to confirm on exit if there are active terminal sessions."), + type: 'boolean', + default: false }, 'terminal.integrated.enableBell': { - 'description': nls.localize('terminal.integrated.enableBell', "Whether the terminal bell is enabled or not."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.enableBell', "Whether the terminal bell is enabled or not."), + type: 'boolean', + default: false }, 'terminal.integrated.commandsToSkipShell': { - 'description': nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open."), - 'type': 'array', - 'items': { - 'type': 'string' + description: nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open."), + type: 'array', + items: { + type: 'string' }, - 'default': [ + default: [ + TERMINAL_COMMAND_ID.CLEAR_SELECTION, + TERMINAL_COMMAND_ID.CLEAR, + TERMINAL_COMMAND_ID.COPY_SELECTION, + TERMINAL_COMMAND_ID.DELETE_WORD_LEFT, + TERMINAL_COMMAND_ID.DELETE_WORD_RIGHT, + TERMINAL_COMMAND_ID.FIND_WIDGET_FOCUS, + TERMINAL_COMMAND_ID.FIND_WIDGET_HIDE, + TERMINAL_COMMAND_ID.FOCUS_NEXT_PANE, + TERMINAL_COMMAND_ID.FOCUS_NEXT, + TERMINAL_COMMAND_ID.FOCUS_PREVIOUS_PANE, + TERMINAL_COMMAND_ID.FOCUS_PREVIOUS, + TERMINAL_COMMAND_ID.FOCUS, + TERMINAL_COMMAND_ID.KILL, + TERMINAL_COMMAND_ID.MOVE_TO_LINE_END, + TERMINAL_COMMAND_ID.MOVE_TO_LINE_START, + TERMINAL_COMMAND_ID.NEW_IN_ACTIVE_WORKSPACE, + TERMINAL_COMMAND_ID.NEW, + TERMINAL_COMMAND_ID.PASTE, + TERMINAL_COMMAND_ID.RESIZE_PANE_DOWN, + TERMINAL_COMMAND_ID.RESIZE_PANE_LEFT, + TERMINAL_COMMAND_ID.RESIZE_PANE_RIGHT, + TERMINAL_COMMAND_ID.RESIZE_PANE_UP, + TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, + TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, + TERMINAL_COMMAND_ID.SCROLL_DOWN_LINE, + TERMINAL_COMMAND_ID.SCROLL_DOWN_PAGE, + TERMINAL_COMMAND_ID.SCROLL_TO_BOTTOM, + TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND, + TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND, + TERMINAL_COMMAND_ID.SCROLL_TO_TOP, + TERMINAL_COMMAND_ID.SCROLL_UP_LINE, + TERMINAL_COMMAND_ID.SCROLL_UP_PAGE, + TERMINAL_COMMAND_ID.SELECT_ALL, + TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND, + TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE, + TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND, + TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE, + TERMINAL_COMMAND_ID.SPLIT_IN_ACTIVE_WORKSPACE, + TERMINAL_COMMAND_ID.SPLIT, + TERMINAL_COMMAND_ID.TOGGLE, ToggleTabFocusModeAction.ID, QUICKOPEN_ACTION_ID, QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, ShowAllCommandsAction.ID, - CreateNewTerminalAction.ID, - CreateNewInActiveWorkspaceTerminalAction.ID, - CopyTerminalSelectionAction.ID, - KillTerminalAction.ID, - FocusActiveTerminalAction.ID, - FocusPreviousTerminalAction.ID, - FocusNextTerminalAction.ID, 'workbench.action.tasks.build', 'workbench.action.tasks.restartTask', 'workbench.action.tasks.runTask', @@ -260,18 +304,6 @@ configurationRegistry.registerConfiguration({ 'workbench.action.focusSixthEditorGroup', 'workbench.action.focusSeventhEditorGroup', 'workbench.action.focusEighthEditorGroup', - TerminalPasteAction.ID, - RunSelectedTextInTerminalAction.ID, - RunActiveFileInTerminalAction.ID, - ToggleTerminalAction.ID, - ScrollDownTerminalAction.ID, - ScrollDownPageTerminalAction.ID, - ScrollToBottomTerminalAction.ID, - ScrollUpTerminalAction.ID, - ScrollUpPageTerminalAction.ID, - ScrollToTopTerminalAction.ID, - ClearTerminalAction.ID, - ClearSelectionTerminalAction.ID, debugActions.StartAction.ID, debugActions.StopAction.ID, debugActions.RunAction.ID, @@ -288,74 +320,54 @@ configurationRegistry.registerConfiguration({ FocusLastGroupAction.ID, OpenFirstEditorInGroup.ID, OpenLastEditorInGroup.ID, - SelectAllTerminalAction.ID, - FocusTerminalFindWidgetAction.ID, - HideTerminalFindWidgetAction.ID, NavigateUpAction.ID, NavigateDownAction.ID, NavigateRightAction.ID, NavigateLeftAction.ID, - DeleteWordLeftTerminalAction.ID, - DeleteWordRightTerminalAction.ID, - MoveToLineStartTerminalAction.ID, - MoveToLineEndTerminalAction.ID, TogglePanelAction.ID, - 'workbench.action.quickOpenView', - SplitTerminalAction.ID, - SplitInActiveWorkspaceTerminalAction.ID, - FocusPreviousPaneTerminalAction.ID, - FocusNextPaneTerminalAction.ID, - ResizePaneLeftTerminalAction.ID, - ResizePaneRightTerminalAction.ID, - ResizePaneUpTerminalAction.ID, - ResizePaneDownTerminalAction.ID, - ScrollToPreviousCommandAction.ID, - ScrollToNextCommandAction.ID, - SelectToPreviousCommandAction.ID, - SelectToNextCommandAction.ID, - SelectToPreviousLineAction.ID, - SelectToNextLineAction.ID + 'workbench.action.quickOpenView' ].sort() }, 'terminal.integrated.env.osx': { - 'description': nls.localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on OS X"), - 'type': 'object', - 'additionalProperties': { - 'type': ['string', 'null'] + description: nls.localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] }, - 'default': {} + default: {} }, 'terminal.integrated.env.linux': { - 'description': nls.localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux"), - 'type': 'object', - 'additionalProperties': { - 'type': ['string', 'null'] + description: nls.localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] }, - 'default': {} + default: {} }, 'terminal.integrated.env.windows': { - 'description': nls.localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows"), - 'type': 'object', - 'additionalProperties': { - 'type': ['string', 'null'] + description: nls.localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] }, - 'default': {} + default: {} }, 'terminal.integrated.showExitAlert': { - 'description': nls.localize('terminal.integrated.showExitAlert', "Show alert `The terminal process terminated with exit code` when exit code is non-zero."), - 'type': 'boolean', - 'default': true + description: nls.localize('terminal.integrated.showExitAlert', "Show alert \"The terminal process terminated with exit code\" when exit code is non-zero."), + type: 'boolean', + default: true }, 'terminal.integrated.experimentalRestore': { - 'description': nls.localize('terminal.integrated.experimentalRestore', "Whether to restore terminal sessions for the workspace automatically when launching VS Code. This is an experimental setting; it may be buggy and could change in the future."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.experimentalRestore', "Whether to restore terminal sessions for the workspace automatically when launching VS Code. This is an experimental setting; it may be buggy and could change or be removed in the future."), + type: 'boolean', + default: false }, + // TODO: Default to dynamic and remove setting in 1.27 'terminal.integrated.experimentalTextureCachingStrategy': { - 'description': nls.localize('terminal.integrated.experimentalTextureCachingStrategy', "Controls how the terminal stores glyph textures. `static` is the default and uses a fixed texture to draw the characters from. `dynamic` will draw the characters to the texture as they are needed, this should boost overall performance at the cost of slightly increased draw time the first time a character is drawn. `dynamic` will eventually become the default and this setting will be removed. Changes to this setting will only apply to new terminals."), - 'type': 'string', - 'enum': ['static', 'dynamic'], - 'default': 'static' + description: nls.localize('terminal.integrated.experimentalTextureCachingStrategy', "Controls how the terminal stores glyph textures. `static` is the default and uses a fixed texture to draw the characters from. `dynamic` will draw the characters to the texture as they are needed, this should boost overall performance at the cost of slightly increased draw time the first time a character is drawn. `dynamic` will eventually become the default and this setting will be removed. Changes to this setting will only apply to new terminals."), + type: 'string', + enum: ['static', 'dynamic'], + default: 'dynamic' }, } }); @@ -368,7 +380,7 @@ registerSingleton(ITerminalService, TerminalService); nls.localize('terminal', "Terminal"), 'terminal', 40, - ToggleTerminalAction.ID + TERMINAL_COMMAND_ID.TOGGLE )); // On mac cmd+` is reserved to cycle between windows, that's why the keybindings use WinCtrl @@ -533,6 +545,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextComm actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousLineAction, SelectToPreviousLineAction.ID, SelectToPreviousLineAction.LABEL), 'Terminal: Select To Previous Line', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextLineAction, SelectToNextLineAction.ID, SelectToNextLineAction.LABEL), 'Terminal: Select To Next Line', category); -terminalCommands.setup(); +setupTerminalCommands(); +setupTerminalMenu(); registerColors(); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 286854eba1b..69e8c6a7ae6 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -26,12 +26,13 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; export const TERMINAL_PICKER_PREFIX = 'term '; export class ToggleTerminalAction extends TogglePanelAction { - public static readonly ID = 'workbench.action.terminal.toggleTerminal'; + public static readonly ID = TERMINAL_COMMAND_ID.TOGGLE; public static readonly LABEL = nls.localize('workbench.action.terminal.toggleTerminal', "Toggle Integrated Terminal"); constructor( @@ -59,7 +60,7 @@ export class ToggleTerminalAction extends TogglePanelAction { export class KillTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.kill'; + public static readonly ID = TERMINAL_COMMAND_ID.KILL; public static readonly LABEL = nls.localize('workbench.action.terminal.kill', "Kill the Active Terminal Instance"); public static readonly PANEL_LABEL = nls.localize('workbench.action.terminal.kill.short', "Kill Terminal"); @@ -84,7 +85,7 @@ export class KillTerminalAction extends Action { export class QuickKillTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.quickKill'; + public static readonly ID = TERMINAL_COMMAND_ID.QUICK_KILL; public static readonly LABEL = nls.localize('workbench.action.terminal.quickKill', "Kill Terminal Instance"); constructor( @@ -110,8 +111,9 @@ export class QuickKillTerminalAction extends Action { */ export class CopyTerminalSelectionAction extends Action { - public static readonly ID = 'workbench.action.terminal.copySelection'; + public static readonly ID = TERMINAL_COMMAND_ID.COPY_SELECTION; public static readonly LABEL = nls.localize('workbench.action.terminal.copySelection', "Copy Selection"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.copySelection.short', "Copy"); constructor( id: string, label: string, @@ -131,7 +133,7 @@ export class CopyTerminalSelectionAction extends Action { export class SelectAllTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectAll'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_ALL; public static readonly LABEL = nls.localize('workbench.action.terminal.selectAll', "Select All"); constructor( @@ -170,7 +172,7 @@ export abstract class BaseSendTextTerminalAction extends Action { } export class DeleteWordLeftTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.deleteWordLeft'; + public static readonly ID = TERMINAL_COMMAND_ID.DELETE_WORD_LEFT; public static readonly LABEL = nls.localize('workbench.action.terminal.deleteWordLeft', "Delete Word Left"); constructor( @@ -184,7 +186,7 @@ export class DeleteWordLeftTerminalAction extends BaseSendTextTerminalAction { } export class DeleteWordRightTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.deleteWordRight'; + public static readonly ID = TERMINAL_COMMAND_ID.DELETE_WORD_RIGHT; public static readonly LABEL = nls.localize('workbench.action.terminal.deleteWordRight', "Delete Word Right"); constructor( @@ -198,7 +200,7 @@ export class DeleteWordRightTerminalAction extends BaseSendTextTerminalAction { } export class MoveToLineStartTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.moveToLineStart'; + public static readonly ID = TERMINAL_COMMAND_ID.MOVE_TO_LINE_START; public static readonly LABEL = nls.localize('workbench.action.terminal.moveToLineStart', "Move To Line Start"); constructor( @@ -212,7 +214,7 @@ export class MoveToLineStartTerminalAction extends BaseSendTextTerminalAction { } export class MoveToLineEndTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.moveToLineEnd'; + public static readonly ID = TERMINAL_COMMAND_ID.MOVE_TO_LINE_END; public static readonly LABEL = nls.localize('workbench.action.terminal.moveToLineEnd', "Move To Line End"); constructor( @@ -227,9 +229,9 @@ export class MoveToLineEndTerminalAction extends BaseSendTextTerminalAction { export class CreateNewTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.new'; + public static readonly ID = TERMINAL_COMMAND_ID.NEW; public static readonly LABEL = nls.localize('workbench.action.terminal.new', "Create New Integrated Terminal"); - public static readonly PANEL_LABEL = nls.localize('workbench.action.terminal.new.short', "New Terminal"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.new.short', "New Terminal"); constructor( id: string, label: string, @@ -280,7 +282,7 @@ export class CreateNewTerminalAction extends Action { export class CreateNewInActiveWorkspaceTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.newInActiveWorkspace'; + public static readonly ID = TERMINAL_COMMAND_ID.NEW_IN_ACTIVE_WORKSPACE; public static readonly LABEL = nls.localize('workbench.action.terminal.newInActiveWorkspace', "Create New Integrated Terminal (In Active Workspace)"); constructor( @@ -301,8 +303,9 @@ export class CreateNewInActiveWorkspaceTerminalAction extends Action { } export class SplitTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.split'; + public static readonly ID = TERMINAL_COMMAND_ID.SPLIT; public static readonly LABEL = nls.localize('workbench.action.terminal.split', "Split Terminal"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.split.short', "Split"); constructor( id: string, label: string, @@ -347,7 +350,7 @@ export class SplitTerminalAction extends Action { } export class SplitInActiveWorkspaceTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.splitInActiveWorkspace'; + public static readonly ID = TERMINAL_COMMAND_ID.SPLIT_IN_ACTIVE_WORKSPACE; public static readonly LABEL = nls.localize('workbench.action.terminal.splitInActiveWorkspace', "Split Terminal (In Active Workspace)"); constructor( @@ -368,7 +371,7 @@ export class SplitInActiveWorkspaceTerminalAction extends Action { } export class FocusPreviousPaneTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusPreviousPane'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_PREVIOUS_PANE; public static readonly LABEL = nls.localize('workbench.action.terminal.focusPreviousPane', "Focus Previous Pane"); constructor( @@ -389,7 +392,7 @@ export class FocusPreviousPaneTerminalAction extends Action { } export class FocusNextPaneTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusNextPane'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_NEXT_PANE; public static readonly LABEL = nls.localize('workbench.action.terminal.focusNextPane', "Focus Next Pane"); constructor( @@ -428,7 +431,7 @@ export abstract class BaseFocusDirectionTerminalAction extends Action { } export class ResizePaneLeftTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneLeft'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_LEFT; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneLeft', "Resize Pane Left"); constructor( @@ -440,7 +443,7 @@ export class ResizePaneLeftTerminalAction extends BaseFocusDirectionTerminalActi } export class ResizePaneRightTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneRight'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_RIGHT; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneRight', "Resize Pane Right"); constructor( @@ -452,7 +455,7 @@ export class ResizePaneRightTerminalAction extends BaseFocusDirectionTerminalAct } export class ResizePaneUpTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneUp'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_UP; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneUp', "Resize Pane Up"); constructor( @@ -464,7 +467,7 @@ export class ResizePaneUpTerminalAction extends BaseFocusDirectionTerminalAction } export class ResizePaneDownTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneDown'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_DOWN; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneDown', "Resize Pane Down"); constructor( @@ -477,7 +480,7 @@ export class ResizePaneDownTerminalAction extends BaseFocusDirectionTerminalActi export class FocusActiveTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focus'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS; public static readonly LABEL = nls.localize('workbench.action.terminal.focus', "Focus Terminal"); constructor( @@ -499,7 +502,7 @@ export class FocusActiveTerminalAction extends Action { export class FocusNextTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusNext'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_NEXT; public static readonly LABEL = nls.localize('workbench.action.terminal.focusNext', "Focus Next Terminal"); constructor( @@ -517,7 +520,7 @@ export class FocusNextTerminalAction extends Action { export class FocusPreviousTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusPrevious'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_PREVIOUS; public static readonly LABEL = nls.localize('workbench.action.terminal.focusPrevious', "Focus Previous Terminal"); constructor( @@ -535,8 +538,9 @@ export class FocusPreviousTerminalAction extends Action { export class TerminalPasteAction extends Action { - public static readonly ID = 'workbench.action.terminal.paste'; + public static readonly ID = TERMINAL_COMMAND_ID.PASTE; public static readonly LABEL = nls.localize('workbench.action.terminal.paste', "Paste into Active Terminal"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.paste.short', "Paste"); constructor( id: string, label: string, @@ -556,8 +560,8 @@ export class TerminalPasteAction extends Action { export class SelectDefaultShellWindowsTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectDefaultShell'; - public static readonly LABEL = nls.localize('workbench.action.terminal.DefaultShell', "Select Default Shell"); + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_DEFAULT_SHELL; + public static readonly LABEL = nls.localize('workbench.action.terminal.selectDefaultShell', "Select Default Shell"); constructor( id: string, label: string, @@ -573,7 +577,7 @@ export class SelectDefaultShellWindowsTerminalAction extends Action { export class RunSelectedTextInTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.runSelectedText'; + public static readonly ID = TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT; public static readonly LABEL = nls.localize('workbench.action.terminal.runSelectedText', "Run Selected Text In Active Terminal"); constructor( @@ -608,7 +612,7 @@ export class RunSelectedTextInTerminalAction extends Action { export class RunActiveFileInTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.runActiveFile'; + public static readonly ID = TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE; public static readonly LABEL = nls.localize('workbench.action.terminal.runActiveFile', "Run Active File In Active Terminal"); constructor( @@ -625,7 +629,7 @@ export class RunActiveFileInTerminalAction extends Action { if (!instance) { return TPromise.as(void 0); } - const editor = this.codeEditorService.getFocusedCodeEditor(); + const editor = this.codeEditorService.getActiveCodeEditor(); if (!editor) { return TPromise.as(void 0); } @@ -641,14 +645,14 @@ export class RunActiveFileInTerminalAction extends Action { export class SwitchTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.switchTerminal'; + public static readonly ID = TERMINAL_COMMAND_ID.SWITCH_TERMINAL; public static readonly LABEL = nls.localize('workbench.action.terminal.switchTerminal', "Switch Terminal"); constructor( id: string, label: string, @ITerminalService private terminalService: ITerminalService ) { - super(SwitchTerminalAction.ID, SwitchTerminalAction.LABEL, 'terminal-action switch-terminal'); + super(id, label, 'terminal-action switch-terminal'); } public run(item?: string): TPromise { @@ -669,7 +673,7 @@ export class SwitchTerminalActionItem extends SelectActionItem { @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, terminalService.getTabLabels(), terminalService.activeTabIndex, contextViewService); + super(null, action, terminalService.getTabLabels(), terminalService.activeTabIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Terminals') }); this.toDispose.push(terminalService.onInstancesChanged(this._updateItems, this)); this.toDispose.push(terminalService.onActiveTabChanged(this._updateItems, this)); @@ -684,7 +688,7 @@ export class SwitchTerminalActionItem extends SelectActionItem { export class ScrollDownTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollDown'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_DOWN_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollDown', "Scroll Down (Line)"); constructor( @@ -705,7 +709,7 @@ export class ScrollDownTerminalAction extends Action { export class ScrollDownPageTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollDownPage'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_DOWN_PAGE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollDownPage', "Scroll Down (Page)"); constructor( @@ -726,7 +730,7 @@ export class ScrollDownPageTerminalAction extends Action { export class ScrollToBottomTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToBottom'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_BOTTOM; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToBottom', "Scroll to Bottom"); constructor( @@ -747,7 +751,7 @@ export class ScrollToBottomTerminalAction extends Action { export class ScrollUpTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollUp'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_UP_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollUp', "Scroll Up (Line)"); constructor( @@ -768,7 +772,7 @@ export class ScrollUpTerminalAction extends Action { export class ScrollUpPageTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollUpPage'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_UP_PAGE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollUpPage', "Scroll Up (Page)"); constructor( @@ -789,7 +793,7 @@ export class ScrollUpPageTerminalAction extends Action { export class ScrollToTopTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToTop'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_TOP; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToTop', "Scroll to Top"); constructor( @@ -810,7 +814,7 @@ export class ScrollToTopTerminalAction extends Action { export class ClearTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.clear'; + public static readonly ID = TERMINAL_COMMAND_ID.CLEAR; public static readonly LABEL = nls.localize('workbench.action.terminal.clear', "Clear"); constructor( @@ -831,7 +835,7 @@ export class ClearTerminalAction extends Action { export class ClearSelectionTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.clearSelection'; + public static readonly ID = TERMINAL_COMMAND_ID.CLEAR_SELECTION; public static readonly LABEL = nls.localize('workbench.action.terminal.clearSelection', "Clear Selection"); constructor( @@ -852,7 +856,7 @@ export class ClearSelectionTerminalAction extends Action { export class AllowWorkspaceShellTerminalCommand extends Action { - public static readonly ID = 'workbench.action.terminal.allowWorkspaceShell'; + public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_ALLOW; public static readonly LABEL = nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration"); constructor( @@ -870,7 +874,7 @@ export class AllowWorkspaceShellTerminalCommand extends Action { export class DisallowWorkspaceShellTerminalCommand extends Action { - public static readonly ID = 'workbench.action.terminal.disallowWorkspaceShell'; + public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_DISALLOW; public static readonly LABEL = nls.localize('workbench.action.terminal.disallowWorkspaceShell', "Disallow Workspace Shell Configuration"); constructor( @@ -888,7 +892,7 @@ export class DisallowWorkspaceShellTerminalCommand extends Action { export class RenameTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.rename'; + public static readonly ID = TERMINAL_COMMAND_ID.RENAME; public static readonly LABEL = nls.localize('workbench.action.terminal.rename', "Rename"); constructor( @@ -918,7 +922,7 @@ export class RenameTerminalAction extends Action { export class FocusTerminalFindWidgetAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusFindWidget'; + public static readonly ID = TERMINAL_COMMAND_ID.FIND_WIDGET_FOCUS; public static readonly LABEL = nls.localize('workbench.action.terminal.focusFindWidget', "Focus Find Widget"); constructor( @@ -935,7 +939,7 @@ export class FocusTerminalFindWidgetAction extends Action { export class HideTerminalFindWidgetAction extends Action { - public static readonly ID = 'workbench.action.terminal.hideFindWidget'; + public static readonly ID = TERMINAL_COMMAND_ID.FIND_WIDGET_HIDE; public static readonly LABEL = nls.localize('workbench.action.terminal.hideFindWidget', "Hide Find Widget"); constructor( @@ -974,7 +978,7 @@ export class QuickOpenActionTermContributor extends ActionBarContributor { export class QuickOpenTermAction extends Action { - public static readonly ID = 'workbench.action.quickOpenTerm'; + public static readonly ID = TERMINAL_COMMAND_ID.QUICK_OPEN_TERM; public static readonly LABEL = nls.localize('quickOpenTerm', "Switch Active Terminal"); constructor( @@ -1013,7 +1017,7 @@ export class RenameTerminalQuickOpenAction extends RenameTerminalAction { } export class ScrollToPreviousCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToPreviousCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToPreviousCommand', "Scroll To Previous Command"); constructor( @@ -1034,7 +1038,7 @@ export class ScrollToPreviousCommandAction extends Action { } export class ScrollToNextCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToNextCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToNextCommand', "Scroll To Next Command"); constructor( @@ -1055,7 +1059,7 @@ export class ScrollToNextCommandAction extends Action { } export class SelectToPreviousCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToPreviousCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToPreviousCommand', "Select To Previous Command"); constructor( @@ -1076,7 +1080,7 @@ export class SelectToPreviousCommandAction extends Action { } export class SelectToNextCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToNextCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToNextCommand', "Select To Next Command"); constructor( @@ -1097,7 +1101,7 @@ export class SelectToNextCommandAction extends Action { } export class SelectToPreviousLineAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToPreviousLine'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"); constructor( @@ -1118,7 +1122,7 @@ export class SelectToPreviousLineAction extends Action { } export class SelectToNextLineAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToNextLine'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"); constructor( @@ -1136,4 +1140,4 @@ export class SelectToNextLineAction extends Action { } return TPromise.as(void 0); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 8bafe8eaaa7..6b98f364797 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -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; @@ -659,6 +665,10 @@ export class TerminalInstance implements ITerminalInstance { const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); this.layout(new dom.Dimension(width, height)); + // HACK: Trigger another async layout to ensure xterm's CharMeasure is ready to use, + // this hack can be removed when https://github.com/xtermjs/xterm.js/issues/702 is + // supported. + setTimeout(() => this.layout(new dom.Dimension(width, height)), 0); } } } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 76750995eac..4e1b4891f83 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -134,7 +134,7 @@ export class TerminalPanel extends Panel { if (!this._actions) { this._actions = [ this._instantiationService.createInstance(SwitchTerminalAction, SwitchTerminalAction.ID, SwitchTerminalAction.LABEL), - this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.PANEL_LABEL), + this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL), this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL), this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL) ]; @@ -147,16 +147,16 @@ export class TerminalPanel extends Panel { private _getContextMenuActions(): IAction[] { if (!this._contextMenuActions) { - this._copyContextMenuAction = this._instantiationService.createInstance(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, nls.localize('copy', "Copy")); + this._copyContextMenuAction = this._instantiationService.createInstance(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.SHORT_LABEL); this._contextMenuActions = [ - this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.PANEL_LABEL), - this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, nls.localize('split', "Split")), + this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL), + this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.SHORT_LABEL), new Separator(), this._copyContextMenuAction, - this._instantiationService.createInstance(TerminalPasteAction, TerminalPasteAction.ID, nls.localize('paste', "Paste")), - this._instantiationService.createInstance(SelectAllTerminalAction, SelectAllTerminalAction.ID, nls.localize('selectAll', "Select All")), + this._instantiationService.createInstance(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.SHORT_LABEL), + this._instantiationService.createInstance(SelectAllTerminalAction, SelectAllTerminalAction.ID, SelectAllTerminalAction.LABEL), new Separator(), - this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, nls.localize('clear', "Clear")) + this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL) ]; this._contextMenuActions.forEach(a => { this._register(a); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index 64a26d1017c..c4d62a87ead 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -3,21 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; -import Uri from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { ITerminalChildProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { TerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/node/terminalProcessExtHostProxy'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalProcess } from 'vs/workbench/parts/terminal/node/terminalProcess'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -50,13 +49,13 @@ export class TerminalProcessManager implements ITerminalProcessManager { public get onProcessExit(): Event { return this._onProcessExit.event; } constructor( - private _terminalId: number, - private _configHelper: ITerminalConfigHelper, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + private readonly _terminalId: number, + private readonly _configHelper: ITerminalConfigHelper, @IHistoryService private readonly _historyService: IHistoryService, - @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILogService private _logService: ILogService + @ILogService private readonly _logService: ILogService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService ) { this.ptyProcessReady = new TPromise(c => { this.onProcessReady(() => { @@ -68,13 +67,11 @@ export class TerminalProcessManager implements ITerminalProcessManager { public dispose(): void { if (this._process) { - if (this._process.connected) { - // If the process was still connected this dispose came from - // within VS Code, not the process, so mark the process as - // killed by the user. - this.processState = ProcessState.KILLED_BY_USER; - this._process.send({ event: 'shutdown' }); - } + // If the process was still connected this dispose came from + // within VS Code, not the process, so mark the process as + // killed by the user. + this.processState = ProcessState.KILLED_BY_USER; + this._process.shutdown(); this._process = null; } this._disposables.forEach(d => d.dispose()); @@ -94,7 +91,6 @@ export class TerminalProcessManager implements ITerminalProcessManager { if (extensionHostOwned) { this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, cols, rows); } else { - const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; if (!shellLaunchConfig.executable) { this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); } @@ -109,23 +105,41 @@ export class TerminalProcessManager implements ITerminalProcessManager { const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); shellLaunchConfig.env = envFromShell; - // Merge process env with the env from config - const parentEnv = { ...process.env }; - terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + // Merge process env with the env from config and from shellLaunchConfig + const env = { ...process.env }; + terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); - // Continue env initialization, merging in the env from the launch - // config and adding keys that are needed to create the process - const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows); - const cwd = Uri.parse(require.toUrl('../node')).fsPath; - const options = { env, cwd }; - this._logService.debug(`Terminal process launching`, options); + // Sanitize the environment, removing any undesirable VS Code and Electron environment + // variables + terminalEnvironment.sanitizeEnvironment(env); - this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + // Adding other env keys necessary to create the process + const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); + + this._logService.debug(`Terminal process launching`, shellLaunchConfig, this.initialCwd, cols, rows, env); + this._process = new TerminalProcess(shellLaunchConfig, this.initialCwd, cols, rows, env); } this.processState = ProcessState.LAUNCHING; - this._process.on('message', message => this._onMessage(message)); - this._process.on('exit', exitCode => this._onExit(exitCode)); + this._process.onProcessData(data => { + this._onProcessData.fire(data); + }); + + this._process.onProcessIdReady(pid => { + this.shellProcessId = pid; + this._onProcessReady.fire(); + + // Send any queued data that's waiting + if (this._preLaunchInputQueue.length > 0) { + this._process.input(this._preLaunchInputQueue.join('')); + this._preLaunchInputQueue.length = 0; + } + }); + + this._process.onProcessTitleChanged(title => this._onProcessTitle.fire(title)); + this._process.onProcessExit(exitCode => this._onExit(exitCode)); setTimeout(() => { if (this.processState === ProcessState.LAUNCHING) { @@ -135,15 +149,17 @@ export class TerminalProcessManager implements ITerminalProcessManager { } public setDimensions(cols: number, rows: number): void { - if (this._process && this._process.connected) { - // The child process could aready be terminated - try { - this._process.send({ event: 'resize', cols, rows }); - } catch (error) { - // We tried to write to a closed pipe / channel. - if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { - throw (error); - } + if (!this._process) { + return; + } + + // The child process could already be terminated + try { + this._process.resize(cols, rows); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); } } } @@ -152,10 +168,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { if (this.shellProcessId) { if (this._process) { // Send data if the pty is ready - this._process.send({ - event: 'input', - data - }); + this._process.input(data); } } else { // If the pty is not ready, queue the data received to send later @@ -163,31 +176,6 @@ export class TerminalProcessManager implements ITerminalProcessManager { } } - private _onMessage(message: IMessageFromTerminalProcess): void { - this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message); - switch (message.type) { - case 'data': - this._onProcessData.fire(message.content); - break; - case 'pid': - this.shellProcessId = message.content; - this._onProcessReady.fire(); - - // Send any queued data that's waiting - if (this._preLaunchInputQueue.length > 0) { - this._process.send({ - event: 'input', - data: this._preLaunchInputQueue.join('') - }); - this._preLaunchInputQueue.length = 0; - } - break; - case 'title': - this._onProcessTitle.fire(message.content); - break; - } - } - private _onExit(exitCode: number): void { this._process = null; diff --git a/src/vs/workbench/parts/terminal/node/terminal.ts b/src/vs/workbench/parts/terminal/node/terminal.ts index 8cb9bc9dddd..8d787d74e93 100644 --- a/src/vs/workbench/parts/terminal/node/terminal.ts +++ b/src/vs/workbench/parts/terminal/node/terminal.ts @@ -7,30 +7,21 @@ import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import * as processes from 'vs/base/node/processes'; import { readFile, fileExists } from 'vs/base/node/pfs'; - -export interface IMessageFromTerminalProcess { - type: 'pid' | 'data' | 'title'; - content: number | string; -} - -export interface IMessageToTerminalProcess { - event: 'resize' | 'input' | 'shutdown'; - data?: string; - cols?: number; - rows?: number; -} +import { Event } from 'vs/base/common/event'; /** * An interface representing a raw terminal child process, this is a subset of the * child_process.ChildProcess node.js interface. */ export interface ITerminalChildProcess { - readonly connected: boolean; + onProcessData: Event; + onProcessExit: Event; + onProcessIdReady: Event; + onProcessTitleChanged: Event; - send(message: IMessageToTerminalProcess): boolean; - - on(event: 'exit', listener: (code: number) => void): this; - on(event: 'message', listener: (message: IMessageFromTerminalProcess) => void): this; + shutdown(): void; + input(data: string): void; + resize(cols: number, rows: number): void; } let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string = null; diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts index 8e2bca6dadd..84c4aa560c4 100644 --- a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -8,7 +8,6 @@ import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import pkg from 'vs/platform/node/package'; import Uri from 'vs/base/common/uri'; -import { IStringDictionary } from 'vs/base/common/collections'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -17,7 +16,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati * This module contains utility functions related to the environment, cwd and paths. */ -export function mergeEnvironments(parent: IStringDictionary, other: IStringDictionary) { +export function mergeEnvironments(parent: platform.IProcessEnvironment, other: platform.IProcessEnvironment): void { if (!other) { return; } @@ -44,7 +43,7 @@ export function mergeEnvironments(parent: IStringDictionary, other: IStr } } -function _mergeEnvironmentValue(env: IStringDictionary, key: string, value: string | null) { +function _mergeEnvironmentValue(env: platform.IProcessEnvironment, key: string, value: string | null): void { if (typeof value === 'string') { env[key] = value; } else { @@ -52,34 +51,44 @@ function _mergeEnvironmentValue(env: IStringDictionary, key: string, val } } -export function createTerminalEnv(parentEnv: IStringDictionary, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary { - const env = { ...parentEnv }; - if (shell.env) { - mergeEnvironments(env, shell.env); - } - - env['PTYPID'] = process.pid.toString(); - env['PTYSHELL'] = shell.executable; - env['TERM_PROGRAM'] = 'vscode'; - env['TERM_PROGRAM_VERSION'] = pkg.version; - if (shell.args) { - if (typeof shell.args === 'string') { - env[`PTYSHELLCMDLINE`] = shell.args; - } else { - shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg); +export function sanitizeEnvironment(env: platform.IProcessEnvironment): void { + // Remove keys based on strings + const keysToRemove = [ + 'ELECTRON_ENABLE_STACK_DUMPING', + 'ELECTRON_ENABLE_LOGGING', + 'ELECTRON_NO_ASAR', + 'ELECTRON_NO_ATTACH_CONSOLE', + 'ELECTRON_RUN_AS_NODE', + 'GOOGLE_API_KEY', + 'VSCODE_CLI', + 'VSCODE_DEV', + 'VSCODE_IPC_HOOK', + 'VSCODE_LOGS', + 'VSCODE_NLS_CONFIG', + 'VSCODE_PORTABLE', + 'VSCODE_PID', + ]; + keysToRemove.forEach((key) => { + if (env[key]) { + delete env[key]; } - } - env['PTYCWD'] = cwd; - env['LANG'] = _getLangEnvVariable(locale); - if (cols && rows) { - env['PTYCOLS'] = cols.toString(); - env['PTYROWS'] = rows.toString(); - } - env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess'; - return env; + }); + + // Remove keys based on regexp + Object.keys(env).forEach(key => { + if (key.search(/^VSCODE_NODE_CACHED_DATA_DIR_\d+$/) === 0) { + delete env[key]; + } + }); } -export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary { +export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, locale: string | undefined): void { + env['TERM_PROGRAM'] = 'vscode'; + env['TERM_PROGRAM_VERSION'] = pkg.version; + env['LANG'] = _getLangEnvVariable(locale); +} + +export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: platform.IProcessEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder): platform.IProcessEnvironment { Object.keys(env).forEach((key) => { if (typeof env[key] === 'string') { env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, env[key]); diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index 8987b358049..5500c17791a 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -5,163 +5,121 @@ import * as os from 'os'; import * as path from 'path'; +import * as platform from 'vs/base/common/platform'; import * as pty from 'node-pty'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; -// The pty process needs to be run in its own child process to get around maxing out CPU on Mac, -// see https://github.com/electron/electron/issues/38 -let shellName: string; -if (os.platform() === 'win32') { - shellName = path.basename(process.env.PTYSHELL); -} else { - // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a - // color prompt as defined in the default ~/.bashrc file. - shellName = 'xterm-256color'; -} -const shell = process.env.PTYSHELL; -const args = getArgs(); -const cwd = process.env.PTYCWD; -const cols = process.env.PTYCOLS; -const rows = process.env.PTYROWS; -let currentTitle = ''; +export class TerminalProcess implements ITerminalChildProcess, IDisposable { + private _exitCode: number; + private _closeTimeout: number; + private _ptyProcess: pty.IPty; + private _currentTitle: string = ''; -setupPlanB(Number(process.env.PTYPID)); -cleanEnv(); + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onProcessTitleChanged: Emitter = new Emitter(); + public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } -interface IOptions { - name: string; - cwd: string; - cols?: number; - rows?: number; -} + constructor( + shellLaunchConfig: IShellLaunchConfig, + cwd: string, + cols: number, + rows: number, + env: platform.IProcessEnvironment + ) { + let shellName: string; + if (os.platform() === 'win32') { + shellName = path.basename(shellLaunchConfig.executable); + } else { + // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a + // color prompt as defined in the default ~/.bashrc file. + shellName = 'xterm-256color'; + } -const options: IOptions = { - name: shellName, - cwd -}; -if (cols && rows) { - options.cols = parseInt(cols, 10); - options.rows = parseInt(rows, 10); -} + const options: pty.IPtyForkOptions = { + name: shellName, + cwd, + env, + cols, + rows + }; -const ptyProcess = pty.spawn(shell, args, options); + this._ptyProcess = pty.spawn(shellLaunchConfig.executable, shellLaunchConfig.args, options); + this._ptyProcess.on('data', (data) => { + this._onProcessData.fire(data); + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + this._queueProcessExit(); + } + }); + this._ptyProcess.on('exit', (code) => { + this._exitCode = code; + this._queueProcessExit(); + }); -let closeTimeout: number; -let exitCode: number; - -// Allow any trailing data events to be sent before the exit event is sent. -// See https://github.com/Tyriar/node-pty/issues/72 -function queueProcessExit() { - if (closeTimeout) { - clearTimeout(closeTimeout); + // TODO: We should no longer need to delay this since pty.spawn is sync + setTimeout(() => { + this._sendProcessId(); + }, 500); + this._setupTitlePolling(); } - closeTimeout = setTimeout(function () { - ptyProcess.kill(); - process.exit(exitCode); - }, 250); -} -ptyProcess.on('data', function (data) { - process.send({ - type: 'data', - content: data - }); - if (closeTimeout) { - clearTimeout(closeTimeout); - queueProcessExit(); + public dispose(): void { + this._onProcessData.dispose(); + this._onProcessExit.dispose(); + this._onProcessIdReady.dispose(); + this._onProcessTitleChanged.dispose(); } -}); -ptyProcess.on('exit', function (code) { - exitCode = code; - queueProcessExit(); -}); + private _setupTitlePolling() { + this._sendProcessTitle(); + setInterval(() => { + if (this._currentTitle !== this._ptyProcess.process) { + this._sendProcessTitle(); + } + }, 200); + } -process.on('message', function (message) { - if (message.event === 'input') { - ptyProcess.write(message.data); - } else if (message.event === 'resize') { + // Allow any trailing data events to be sent before the exit event is sent. + // See https://github.com/Tyriar/node-pty/issues/72 + private _queueProcessExit() { + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + } + this._closeTimeout = setTimeout(() => { + this._ptyProcess.kill(); + this._onProcessExit.fire(this._exitCode); + this.dispose(); + }, 250); + } + + private _sendProcessId() { + this._onProcessIdReady.fire(this._ptyProcess.pid); + } + + private _sendProcessTitle(): void { + this._currentTitle = this._ptyProcess.process; + this._onProcessTitleChanged.fire(this._currentTitle); + } + + public shutdown(): void { + this._queueProcessExit(); + } + + public input(data: string): void { + this._ptyProcess.write(data); + } + + public resize(cols: number, rows: number): void { // Ensure that cols and rows are always >= 1, this prevents a native // exception in winpty. - ptyProcess.resize(Math.max(message.cols, 1), Math.max(message.rows, 1)); - } else if (message.event === 'shutdown') { - queueProcessExit(); - } -}); - -sendProcessId(); -setupTitlePolling(); - -function getArgs(): string | string[] { - if (process.env['PTYSHELLCMDLINE']) { - return process.env['PTYSHELLCMDLINE']; - } - const args = []; - let i = 0; - while (process.env['PTYSHELLARG' + i]) { - args.push(process.env['PTYSHELLARG' + i]); - i++; - } - return args; -} - -function cleanEnv() { - const keys = [ - 'AMD_ENTRYPOINT', - 'ELECTRON_NO_ASAR', - 'ELECTRON_RUN_AS_NODE', - 'GOOGLE_API_KEY', - 'PTYCWD', - 'PTYPID', - 'PTYSHELL', - 'PTYCOLS', - 'PTYROWS', - 'PTYSHELLCMDLINE', - 'VSCODE_LOGS', - 'VSCODE_PORTABLE', - 'VSCODE_PID', - ]; - keys.forEach(function (key) { - if (process.env[key]) { - delete process.env[key]; - } - }); - let i = 0; - while (process.env['PTYSHELLARG' + i]) { - delete process.env['PTYSHELLARG' + i]; - i++; + this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1)); } } - -function setupPlanB(parentPid: number) { - setInterval(function () { - try { - process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore. - } catch (e) { - process.exit(); - } - }, 5000); -} - -function sendProcessId() { - process.send({ - type: 'pid', - content: ptyProcess.pid - }); -} - -function setupTitlePolling() { - sendProcessTitle(); - setInterval(function () { - if (currentTitle !== ptyProcess.process) { - sendProcessTitle(); - } - }, 200); -} - -function sendProcessTitle() { - process.send({ - type: 'title', - content: ptyProcess.process - }); - currentTitle = ptyProcess.process; -} diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts index 9c842fbbfa8..52bc33c6fa5 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -3,17 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITerminalChildProcess, IMessageToTerminalProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; -import { EventEmitter } from 'events'; +import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { Event, Emitter } from 'vs/base/common/event'; import { ITerminalService, ITerminalProcessExtHostProxy, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; - -export class TerminalProcessExtHostProxy extends EventEmitter implements ITerminalChildProcess, ITerminalProcessExtHostProxy { - // For ext host processes connected checks happen on the ext host - public connected: boolean = true; +import { IDisposable } from 'vs/base/common/lifecycle'; +export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerminalProcessExtHostProxy { private _disposables: IDisposable[] = []; + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onProcessTitleChanged: Emitter = new Emitter(); + public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } + + private readonly _onInput: Emitter = new Emitter(); + public get onInput(): Event { return this._onInput.event; } + private readonly _onResize: Emitter<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>(); + public get onResize(): Event<{ cols: number, rows: number }> { return this._onResize.event; } + private readonly _onShutdown: Emitter = new Emitter(); + public get onShutdown(): Event { return this._onShutdown.event; } + constructor( public terminalId: number, shellLaunchConfig: IShellLaunchConfig, @@ -21,8 +34,6 @@ export class TerminalProcessExtHostProxy extends EventEmitter implements ITermin rows: number, @ITerminalService private _terminalService: ITerminalService ) { - super(); - // TODO: Return TPromise indicating success? Teardown if failure? this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows); } @@ -33,46 +44,30 @@ export class TerminalProcessExtHostProxy extends EventEmitter implements ITermin } public emitData(data: string): void { - this.emit('message', { type: 'data', content: data } as IMessageFromTerminalProcess); + this._onProcessData.fire(data); } public emitTitle(title: string): void { - this.emit('message', { type: 'title', content: title } as IMessageFromTerminalProcess); + this._onProcessTitleChanged.fire(title); } public emitPid(pid: number): void { - this.emit('message', { type: 'pid', content: pid } as IMessageFromTerminalProcess); + this._onProcessIdReady.fire(pid); } public emitExit(exitCode: number): void { - this.emit('exit', exitCode); + this._onProcessExit.fire(exitCode); this.dispose(); } - - public send(message: IMessageToTerminalProcess): boolean { - switch (message.event) { - case 'input': this.emit('input', message.data); break; - case 'resize': this.emit('resize', message.cols, message.rows); break; - case 'shutdown': this.emit('shutdown'); break; - } - return true; + public shutdown(): void { + this._onShutdown.fire(); } - public onInput(listener: (data: string) => void): void { - const outerListener = (data) => listener(data); - this.on('input', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('input', outerListener))); + public input(data: string): void { + this._onInput.fire(data); } - public onResize(listener: (cols: number, rows: number) => void): void { - const outerListener = (cols, rows) => listener(cols, rows); - this.on('resize', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('resize', outerListener))); - } - - public onShutdown(listener: () => void): void { - const outerListener = () => listener(); - this.on('shutdown', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('shutdown', outerListener))); + public resize(cols: number, rows: number): void { + this._onResize.fire({ cols, rows }); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts index 5fae93b3d01..1fad535bd47 100644 --- a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts @@ -9,45 +9,47 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; import Uri from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; -import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; suite('Workbench - TerminalEnvironment', () => { - test('createTerminalEnv', function () { - const shell1 = { - executable: '/bin/foosh', - args: ['-bar', 'baz'] - }; - const parentEnv1: IStringDictionary = { - ok: true - } as any; - const env1 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au'); - assert.ok(env1['ok'], 'Parent environment is copied'); - assert.deepStrictEqual(parentEnv1, { ok: true }, 'Parent environment is unchanged'); - assert.equal(env1['PTYPID'], process.pid.toString(), 'PTYPID is equal to the current PID'); - assert.equal(env1['PTYSHELL'], '/bin/foosh', 'PTYSHELL is equal to the provided shell'); - assert.equal(env1['PTYSHELLARG0'], '-bar', 'PTYSHELLARG0 is equal to the first shell argument'); - assert.equal(env1['PTYSHELLARG1'], 'baz', 'PTYSHELLARG1 is equal to the first shell argument'); - assert.ok(!('PTYSHELLARG2' in env1), 'PTYSHELLARG2 is unset'); - assert.equal(env1['PTYCWD'], '/foo', 'PTYCWD is equal to requested cwd'); - assert.equal(env1['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + test('addTerminalEnvironmentKeys', () => { + const env = { FOO: 'bar' }; + const locale = 'en-au'; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); + assert.equal(env['TERM_PROGRAM'], 'vscode'); + assert.equal(env['TERM_PROGRAM_VERSION'].search(/^\d+\.\d+\.\d+$/), 0); + assert.equal(env['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); - const shell2: IShellLaunchConfig = { - executable: '/bin/foosh', - args: [] - }; - const parentEnv2: IStringDictionary = { - LANG: 'en_US.UTF-8' - }; - const env2 = terminalEnvironment.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au'); - assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset'); - assert.equal(env2['PTYCWD'], '/foo', 'PTYCWD is equal to /foo'); - assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + const env2 = { FOO: 'bar' }; + terminalEnvironment.addTerminalEnvironmentKeys(env2, null); + assert.equal(env2['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 - const env3 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/', null); - assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 + const env3 = { LANG: 'en_US.UTF-8' }; + terminalEnvironment.addTerminalEnvironmentKeys(env3, null); + assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); + }); - const env4 = terminalEnvironment.createTerminalEnv(parentEnv2, shell1, '/', null); - assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); + test('sanitizeEnvironment', () => { + let env = { + FOO: 'bar', + ELECTRON_ENABLE_STACK_DUMPING: 'x', + ELECTRON_ENABLE_LOGGING: 'x', + ELECTRON_NO_ASAR: 'x', + ELECTRON_NO_ATTACH_CONSOLE: 'x', + ELECTRON_RUN_AS_NODE: 'x', + GOOGLE_API_KEY: 'x', + VSCODE_CLI: 'x', + VSCODE_DEV: 'x', + VSCODE_IPC_HOOK: 'x', + VSCODE_LOGS: 'x', + VSCODE_NLS_CONFIG: 'x', + VSCODE_PORTABLE: 'x', + VSCODE_PID: 'x', + VSCODE_NODE_CACHED_DATA_DIR_12345: 'x' + }; + terminalEnvironment.sanitizeEnvironment(env); + assert.equal(env['FOO'], 'bar'); + assert.equal(Object.keys(env).length, 1); }); suite('mergeEnvironments', () => { 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 4d2291134ef..dfa15ae450f 100644 --- a/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts +++ b/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts @@ -10,7 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; import { firstIndex } from 'vs/base/common/arrays'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; @@ -228,3 +228,21 @@ const developerCategory = localize('developer', "Developer"); const generateColorThemeDescriptor = new SyncActionDescriptor(GenerateColorThemeAction, GenerateColorThemeAction.ID, GenerateColorThemeAction.LABEL); Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(generateColorThemeDescriptor, 'Developer: Generate Color Theme From Current Settings', developerCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '4_themes', + command: { + id: SelectColorThemeAction.ID, + title: localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '4_themes', + command: { + id: SelectIconThemeAction.ID, + title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme") + }, + order: 2 +}); 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 dd4c68f0af4..941351830b6 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -21,7 +21,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; +import { IUpdateService, State as UpdateState, StateType, IUpdate, UpdateType } from 'vs/platform/update/common/update'; import * as semver from 'semver'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INotificationService, INotificationHandle } from 'vs/platform/notification/common/notification'; @@ -209,6 +209,80 @@ 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'; + + private disposables: IDisposable[] = []; + + constructor( + @IStorageService private storageService: IStorageService, + @INotificationService private notificationService: INotificationService, + @IEnvironmentService private environmentService: IEnvironmentService, + @IOpenerService private openerService: IOpenerService, + @IUpdateService private updateService: IUpdateService + ) { + updateService.onStateChange(this.onUpdateStateChange, this, this.disposables); + this.onUpdateStateChange(this.updateService.state); + } + + private onUpdateStateChange(state: UpdateState): void { + if (state.type !== StateType.Idle) { + return; + } + + if (state.updateType !== UpdateType.Setup) { + return; + } + + if (!this.environmentService.isBuilt || this.environmentService.disableUpdates) { + return; + } + + const neverShowAgain = new NeverShowAgain(WinUserSetupContribution.KEY, this.storageService); + + if (!neverShowAgain.shouldShow()) { + return; + } + + const handle = this.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(); + } + }] + ); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + class CommandAction extends Action { constructor( @@ -223,7 +297,7 @@ class CommandAction extends Action { export class UpdateContribution implements IGlobalActivity { private static readonly showCommandsId = 'workbench.action.showCommands'; - private static readonly openSettingsId = 'workbench.action.openSettings'; + private static readonly openSettingsId = 'workbench.action.openSettings2'; private static readonly openKeybindingsId = 'workbench.action.openGlobalKeybindings'; private static readonly openUserSnippets = 'workbench.action.openSnippets'; private static readonly selectColorThemeId = 'workbench.action.selectTheme'; diff --git a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts index 44449b32df2..0c2214ba8ef 100644 --- a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts +++ b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts @@ -24,9 +24,9 @@ import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/comm import { Parts, IPartService, IDimension } from 'vs/workbench/services/part/common/partService'; import { StartAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { FindInFilesActionId } from 'vs/workbench/parts/search/common/constants'; -import { ToggleTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { escape } from 'vs/base/common/strings'; import { QUICKOPEN_ACTION_ID } from 'vs/workbench/browser/parts/quickopen/quickopen'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; interface WatermarkEntry { text: string; @@ -68,7 +68,7 @@ const newUntitledFile: WatermarkEntry = { const newUntitledFileMacOnly: WatermarkEntry = assign({ mac: true }, newUntitledFile); const toggleTerminal: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), - ids: [ToggleTerminalAction.ID] + ids: [TERMINAL_COMMAND_ID.TOGGLE] }; const findInFiles: WatermarkEntry = { diff --git a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts index 49a9d309bbf..32cd90f5570 100644 --- a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts +++ b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts @@ -14,6 +14,8 @@ import URI from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IExperimentService, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TelemetryOptOut implements IWorkbenchContribution { @@ -25,20 +27,45 @@ export class TelemetryOptOut implements IWorkbenchContribution { @INotificationService notificationService: INotificationService, @IWindowService windowService: IWindowService, @IWindowsService windowsService: IWindowsService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IExperimentService experimentService: IExperimentService, + @IConfigurationService configurationService: IConfigurationService ) { if (!product.telemetryOptOutUrl || storageService.get(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN)) { return; } + const experimentId = 'telemetryOptOut'; Promise.all([ windowService.isFocused(), - windowsService.getWindowCount() - ]).then(([focused, count]) => { + windowsService.getWindowCount(), + experimentService.getExperimentById(experimentId) + ]).then(([focused, count, experimentState]) => { if (!focused && count > 1) { return null; } storageService.store(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN, true); + if (experimentState && experimentState.state === ExperimentState.Run && telemetryService.isOptedIn) { + notificationService.prompt( + Severity.Info, + localize('telemetryOptOut.optOutOption', "Microsoft collects usage data to improve VS Code. You may choose to opt out."), + [ + { + label: localize('telemetryOptOut.OptOut', "Opt out"), + run: () => { + configurationService.updateValue('telemetry.enableTelemetry', false); + configurationService.updateValue('telemetry.enableCrashReporter', false); + } + }, + { + label: localize('telemetryOptOut.readMore', "Read More"), + run: () => openerService.open(URI.parse(product.telemetryOptOutUrl)) + }] + ); + experimentService.markAsCompleted(experimentId); + return; + } + const optOutUrl = product.telemetryOptOutUrl; const privacyUrl = product.privacyStatementUrl || product.telemetryOptOutUrl; const optOutNotice = localize('telemetryOptOut.optOutNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt out]({1}).", privacyUrl, optOutUrl); 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 e9860c97d85..dff4b7f549b 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -24,21 +24,22 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Schemas } from 'vs/base/common/network'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { getInstalledExtensions, IExtensionStatus, onExtensionChanged, isKeymapExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils'; -import { IExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, EnablementState, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; import { used } from 'vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page'; import { ILifecycleService, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { tildify, getBaseLabel } from 'vs/base/common/labels'; +import { tildify, getBaseLabel, getPathLabel } from 'vs/base/common/labels'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughUtils'; import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; 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'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; used(); @@ -277,33 +278,40 @@ class WelcomePage { const before = ul.firstElementChild; workspaces.slice(0, 5).forEach(workspace => { let label: string; - let parent: string; - let wsPath: string; + let resource: URI; if (isSingleFolderWorkspaceIdentifier(workspace)) { - label = getBaseLabel(workspace); - parent = path.dirname(workspace); - wsPath = workspace; - } else { + resource = workspace; label = getWorkspaceLabel(workspace, this.environmentService); - parent = path.dirname(workspace.configPath); - wsPath = workspace.configPath; + } else if (isWorkspaceIdentifier(workspace)) { + label = getWorkspaceLabel(workspace, this.environmentService); + resource = URI.file(workspace.configPath); + } else { + label = getBaseLabel(workspace); + resource = URI.file(workspace); } const li = document.createElement('li'); const a = document.createElement('a'); let name = label; - let parentFolder = parent; - if (!name && parentFolder) { - const tmp = name; - name = parentFolder; - parentFolder = tmp; + let parentFolderPath: string; + + if (resource.scheme === Schemas.file) { + let parentFolder = path.dirname(resource.fsPath); + if (!name && parentFolder) { + const tmp = name; + name = parentFolder; + parentFolder = tmp; + } + parentFolderPath = tildify(parentFolder, this.environmentService.userHome); + } else { + parentFolderPath = getPathLabel(resource, this.environmentService); } - const tildifiedParentFolder = tildify(parentFolder, this.environmentService.userHome); + a.innerText = name; a.title = label; - a.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, tildifiedParentFolder)); + a.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentFolderPath)); a.href = 'javascript:void(0)'; a.addEventListener('click', e => { /* __GDPR__ @@ -316,7 +324,7 @@ class WelcomePage { id: 'openRecentFolder', from: telemetryFrom }); - this.windowService.openWindow([wsPath], { forceNewWindow: e.ctrlKey || e.metaKey }); + this.windowService.openWindow([resource], { forceNewWindow: e.ctrlKey || e.metaKey }); e.preventDefault(); e.stopPropagation(); }); @@ -325,7 +333,7 @@ class WelcomePage { const span = document.createElement('span'); span.classList.add('path'); span.classList.add('detail'); - span.innerText = tildifiedParentFolder; + span.innerText = parentFolderPath; span.title = label; li.appendChild(span); @@ -420,7 +428,9 @@ class WelcomePage { return null; } return this.extensionManagementService.installFromGallery(extension) - .then(local => { + .then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User)) + .then(installed => { + const local = installed.filter(i => areSameExtensions(extension.identifier, i.galleryIdentifier))[0]; // TODO: Do this as part of the install to avoid multiple events. return this.extensionEnablementService.setEnablement(local, EnablementState.Disabled).then(() => local); }); 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 19924b0b073..625b1675d72 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts @@ -10,7 +10,7 @@ import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableEle import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -117,7 +117,7 @@ export class WalkThroughPart extends BaseEditor { private addEventListener(element: E, type: string, listener: EventListenerOrEventListenerObject, useCapture?: boolean): IDisposable; private addEventListener(element: E, type: string, listener: EventListenerOrEventListenerObject, useCapture?: boolean): IDisposable { element.addEventListener(type, listener, useCapture); - return { dispose: () => { element.removeEventListener(type, listener, useCapture); } }; + return toDisposable(() => { element.removeEventListener(type, listener, useCapture); }); } private registerFocusHandlers() { diff --git a/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts index a23fcba984b..a0a69bf0c88 100644 --- a/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts @@ -14,7 +14,6 @@ import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { isResourceFileEdit, isResourceTextEdit, ResourceFileEdit, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -48,30 +47,23 @@ abstract class Recording { abstract hasChanged(resource: URI): boolean; } -class EditTask implements IDisposable { +class ModelEditTask implements IDisposable { - private _initialSelections: Selection[]; - private _endCursorSelection: Selection; - private get _model(): ITextModel { return this._modelReference.object.textEditorModel; } - private _modelReference: IReference; - private _edits: IIdentifiedSingleEditOperation[]; - private _newEol: EndOfLineSequence; + private readonly _model: ITextModel; - constructor(modelReference: IReference) { - this._endCursorSelection = null; - this._modelReference = modelReference; + protected _edits: IIdentifiedSingleEditOperation[]; + protected _newEol: EndOfLineSequence; + + constructor(private readonly _modelReference: IReference) { + this._model = this._modelReference.object.textEditorModel; this._edits = []; } dispose() { - if (this._model) { - this._modelReference.dispose(); - this._modelReference = null; - } + dispose(this._modelReference); } addEdit(resourceEdit: ResourceTextEdit): void { - for (const edit of resourceEdit.edits) { if (typeof edit.eol === 'number') { // honor eol-change @@ -93,9 +85,8 @@ class EditTask implements IDisposable { apply(): void { if (this._edits.length > 0) { this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - this._initialSelections = this._getInitialSelections(); this._model.pushStackElement(); - this._model.pushEditOperations(this._initialSelections, this._edits, (edits) => this._getEndCursorSelections(edits)); + this._model.pushEditOperations([], this._edits, () => []); this._model.pushStackElement(); } if (this._newEol !== undefined) { @@ -104,58 +95,29 @@ class EditTask implements IDisposable { this._model.pushStackElement(); } } - - protected _getInitialSelections(): Selection[] { - const firstRange = this._edits[0].range; - const initialSelection = new Selection( - firstRange.startLineNumber, - firstRange.startColumn, - firstRange.endLineNumber, - firstRange.endColumn - ); - return [initialSelection]; - } - - private _getEndCursorSelections(inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] { - let relevantEditIndex = 0; - for (let i = 0; i < inverseEditOperations.length; i++) { - const editRange = inverseEditOperations[i].range; - for (let j = 0; j < this._initialSelections.length; j++) { - const selectionRange = this._initialSelections[j]; - if (Range.areIntersectingOrTouching(editRange, selectionRange)) { - relevantEditIndex = i; - break; - } - } - } - - const srcRange = inverseEditOperations[relevantEditIndex].range; - this._endCursorSelection = new Selection( - srcRange.endLineNumber, - srcRange.endColumn, - srcRange.endLineNumber, - srcRange.endColumn - ); - return [this._endCursorSelection]; - } - - getEndCursorSelection(): Selection { - return this._endCursorSelection; - } - } -class SourceModelEditTask extends EditTask { +class EditorEditTask extends ModelEditTask { - private _knownInitialSelections: Selection[]; + private _editor: ICodeEditor; - constructor(modelReference: IReference, initialSelections: Selection[]) { + constructor(modelReference: IReference, editor: ICodeEditor) { super(modelReference); - this._knownInitialSelections = initialSelections; + this._editor = editor; } - protected _getInitialSelections(): Selection[] { - return this._knownInitialSelections; + apply(): void { + if (this._edits.length > 0) { + this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + this._editor.pushUndoStop(); + this._editor.executeEdits('', this._edits); + this._editor.pushUndoStop(); + } + if (this._newEol !== undefined) { + this._editor.pushUndoStop(); + this._editor.getModel().pushEOL(this._newEol); + this._editor.pushUndoStop(); + } } } @@ -163,10 +125,8 @@ class BulkEditModel implements IDisposable { private _textModelResolverService: ITextModelService; private _edits = new Map(); - private _tasks: EditTask[]; - private _sourceModel: URI; - private _sourceSelections: Selection[]; - private _sourceModelTask: SourceModelEditTask; + private _editor: ICodeEditor; + private _tasks: ModelEditTask[]; private _progress: IProgress; constructor( @@ -176,9 +136,7 @@ class BulkEditModel implements IDisposable { progress: IProgress ) { this._textModelResolverService = textModelResolverService; - this._sourceModel = editor ? editor.getModel().uri : undefined; - this._sourceSelections = editor ? editor.getSelections() : undefined; - this._sourceModelTask = undefined; + this._editor = editor; this._progress = progress; edits.forEach(this.addEdit, this); @@ -214,12 +172,11 @@ class BulkEditModel implements IDisposable { throw new Error(`Cannot load file ${key}`); } - let task: EditTask; - if (this._sourceModel && model.textEditorModel.uri.toString() === this._sourceModel.toString()) { - this._sourceModelTask = new SourceModelEditTask(ref, this._sourceSelections); - task = this._sourceModelTask; + let task: ModelEditTask; + if (this._editor && this._editor.getModel().uri.toString() === model.textEditorModel.uri.toString()) { + task = new EditorEditTask(ref, this._editor); } else { - task = new EditTask(ref); + task = new ModelEditTask(ref); } value.forEach(edit => task.addEdit(edit)); @@ -234,14 +191,11 @@ class BulkEditModel implements IDisposable { return this; } - apply(): Selection { + apply(): void { for (const task of this._tasks) { task.apply(); this._progress.report(undefined); } - return this._sourceModelTask - ? this._sourceModelTask.getEndCursorSelection() - : undefined; } } @@ -287,7 +241,7 @@ export class BulkEdit { } } - async perform(): Promise { + async perform(): Promise { let seen = new Set(); let total = 0; @@ -317,17 +271,14 @@ export class BulkEdit { this._progress.total(total); let progress: IProgress = { report: _ => this._progress.worked(1) }; - // do it. return the last selection computed - // by a text change (can be undefined then) - let res: Selection = undefined; + // do it. for (const group of groups) { if (isResourceFileEdit(group[0])) { await this._performFileEdits(group, progress); } else { - res = await this._performTextEdits(group, progress) || res; + await this._performTextEdits(group, progress); } } - return res; } private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { @@ -335,21 +286,32 @@ export class BulkEdit { for (const edit of edits) { progress.report(undefined); - let overwrite = edit.options && edit.options.overwrite; + let options = edit.options || {}; + if (edit.newUri && edit.oldUri) { - await this._textFileService.move(edit.oldUri, edit.newUri, overwrite); - } else if (!edit.newUri && edit.oldUri) { - await this._textFileService.delete(edit.oldUri, { useTrash: true, recursive: edit.options && edit.options.recursive }); - } else if (edit.newUri && !edit.oldUri) { - let ignoreIfExists = edit.options && edit.options.ignoreIfExists; - if (!ignoreIfExists || !await this._fileService.existsFile(edit.newUri)) { - await this._textFileService.create(edit.newUri, undefined, { overwrite }); + // rename + if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists } + await this._textFileService.move(edit.oldUri, edit.newUri, options.overwrite); + + } else if (!edit.newUri && edit.oldUri) { + // delete file + if (!options.ignoreIfNotExists || await this._fileService.existsFile(edit.oldUri)) { + await this._textFileService.delete(edit.oldUri, { useTrash: true, recursive: options.recursive }); + } + + } else if (edit.newUri && !edit.oldUri) { + // create file + if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists + } + await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite }); } } } - private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { + private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const recording = Recording.start(this._fileService); @@ -368,9 +330,8 @@ export class BulkEdit { throw new Error(localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', '))); } - const selection = await model.apply(); + await model.apply(); model.dispose(); - return selection; } } @@ -420,8 +381,8 @@ export class BulkEditService implements IBulkEditService { const bulkEdit = new BulkEdit(options.editor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._environmentService, this._contextService); bulkEdit.add(edits); - return TPromise.wrap(bulkEdit.perform().then(selection => { - return { selection, ariaSummary: bulkEdit.ariaMessage() }; + return TPromise.wrap(bulkEdit.perform().then(() => { + return { ariaSummary: bulkEdit.ariaMessage() }; }, err => { // console.log('apply FAILED'); // console.log(err); diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index f464bea68ba..0ba296dc6db 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -26,7 +26,7 @@ import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSetti import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { createHash } from 'crypto'; -import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { getWorkspaceLabel, IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -38,9 +38,9 @@ import { JSONEditingService } from 'vs/workbench/services/configuration/node/jso import { Schemas } from 'vs/base/common/network'; import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; -import { getBaseLabel } from 'vs/base/common/labels'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; +import { isEqual, hasToIgnoreCase } from 'vs/base/common/resources'; export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { @@ -131,7 +131,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { switch (this.getWorkbenchState()) { case WorkbenchState.FOLDER: - return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier); + return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && isEqual(workspaceIdentifier, this.workspace.folders[0].uri, hasToIgnoreCase(workspaceIdentifier)); case WorkbenchState.WORKSPACE: return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id; } @@ -322,7 +322,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this.jsonEditingService = instantiationService.createInstance(JSONEditingService); } - private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise { + private createWorkspace(arg: IWorkspaceIdentifier | URI | IWindowConfiguration): TPromise { if (isWorkspaceIdentifier(arg)) { return this.createMulitFolderWorkspace(arg); } @@ -345,15 +345,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat }); } - private createSingleFolderWorkspace(singleFolderWorkspaceIdentifier: ISingleFolderWorkspaceIdentifier): TPromise { - const folderPath = URI.file(singleFolderWorkspaceIdentifier); - return stat(folderPath.fsPath) - .then(workspaceStat => { - const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead! - const id = createHash('md5').update(folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex'); - const folder = URI.file(folderPath.fsPath); - return new Workspace(id, getBaseLabel(folder), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime); - }); + private createSingleFolderWorkspace(folder: URI): TPromise { + if (folder.scheme === Schemas.file) { + return stat(folder.fsPath) + .then(workspaceStat => { + const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead! + const id = createHash('md5').update(folder.fsPath).update(ctime ? String(ctime) : '').digest('hex'); + return new Workspace(id, getWorkspaceLabel(folder, this.environmentService), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime); + }); + } else { + const id = createHash('md5').update(folder.toString()).digest('hex'); + return TPromise.as(new Workspace(id, getWorkspaceLabel(folder, this.environmentService), toWorkspaceFolders([{ uri: folder.toString() }]), null)); + } } private createEmptyWorkspace(configuration: IWindowConfiguration): TPromise { @@ -670,15 +673,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat } return {}; } - - private pathEquals(path1: string, path2: string): boolean { - if (!isLinux) { - path1 = path1.toLowerCase(); - path2 = path2.toLowerCase(); - } - - return path1 === path2; - } } interface IExportedConfigurationNode { diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index 0e42aaccf0c..a6ab72cead7 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -38,6 +38,7 @@ import { mkdirp } from 'vs/base/node/pfs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { CommandService } from 'vs/workbench/services/commands/common/commandService'; +import URI from 'vs/base/common/uri'; class SettingsTestEnvironmentService extends EnvironmentService { @@ -103,7 +104,7 @@ suite('ConfigurationEditingService', () => { instantiationService.stub(IEnvironmentService, environmentService); const workspaceService = new WorkspaceService(environmentService); instantiationService.stub(IWorkspaceContextService, workspaceService); - return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => { + return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : URI.file(workspaceDir)).then(() => { instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index a38560e14b4..64e9352f99a 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -87,7 +87,7 @@ suite('WorkspaceContextService - Folder', () => { const globalSettingsFile = path.join(parentDir, 'settings.json'); const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); workspaceContextService = new WorkspaceService(environmentService); - return (workspaceContextService).initialize(folderDir); + return (workspaceContextService).initialize(URI.file(folderDir)); }); }); @@ -124,11 +124,11 @@ suite('WorkspaceContextService - Folder', () => { }); test('isCurrentWorkspace() => true', () => { - assert.ok(workspaceContextService.isCurrentWorkspace(workspaceResource)); + assert.ok(workspaceContextService.isCurrentWorkspace(URI.file(workspaceResource))); }); test('isCurrentWorkspace() => false', () => { - assert.ok(!workspaceContextService.isCurrentWorkspace(workspaceResource + 'abc')); + assert.ok(!workspaceContextService.isCurrentWorkspace(URI.file(workspaceResource + 'abc'))); }); }); @@ -445,7 +445,7 @@ suite('WorkspaceService - Initialization', () => { testObject.onDidChangeWorkspaceFolders(target); testObject.onDidChangeConfiguration(target); - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue'); assert.equal(target.callCount, 3); @@ -474,7 +474,7 @@ suite('WorkspaceService - Initialization', () => { fs.writeFileSync(path.join(parentResource, '1', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue" }'); - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue'); assert.equal(target.callCount, 4); @@ -548,7 +548,7 @@ suite('WorkspaceService - Initialization', () => { test('initialize a folder workspace from a folder workspace with no configuration changes', () => { - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { fs.writeFileSync(globalSettingsFile, '{ "initialization.testSetting1": "userValue" }'); @@ -560,7 +560,7 @@ suite('WorkspaceService - Initialization', () => { testObject.onDidChangeWorkspaceFolders(target); testObject.onDidChangeConfiguration(target); - return testObject.initialize(path.join(parentResource, '2')) + return testObject.initialize(URI.file(path.join(parentResource, '2'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue'); assert.equal(target.callCount, 1); @@ -576,7 +576,7 @@ suite('WorkspaceService - Initialization', () => { test('initialize a folder workspace from a folder workspace with configuration changes', () => { - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { const target = sinon.spy(); @@ -586,7 +586,7 @@ suite('WorkspaceService - Initialization', () => { testObject.onDidChangeConfiguration(target); fs.writeFileSync(path.join(parentResource, '2', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue2" }'); - return testObject.initialize(path.join(parentResource, '2')) + return testObject.initialize(URI.file(path.join(parentResource, '2'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue2'); assert.equal(target.callCount, 2); @@ -601,7 +601,7 @@ suite('WorkspaceService - Initialization', () => { test('initialize a multi folder workspace from a folder workspacce triggers change events in the right order', () => { const folderDir = path.join(parentResource, '1'); - return testObject.initialize(folderDir) + return testObject.initialize(URI.file(folderDir)) .then(() => { const target = sinon.spy(); @@ -666,7 +666,7 @@ suite('WorkspaceConfigurationService - Folder', () => { instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IEnvironmentService, environmentService); - return workspaceService.initialize(folderDir).then(() => { + return workspaceService.initialize(URI.file(folderDir)).then(() => { const fileService = new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); instantiationService.stub(IFileService, fileService); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 2c0ede219c9..124d039a302 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -14,6 +14,7 @@ import { ConfigurationResolverService } from 'vs/workbench/services/configuratio import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestEnvironmentService, TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { Disposable } from 'vs/base/common/lifecycle'; suite('Configuration Resolver Service', () => { let configurationResolverService: IConfigurationResolverService; @@ -391,7 +392,7 @@ class MockCommandService implements ICommandService { public _serviceBrand: any; public callCount = 0; - onWillExecuteCommand = () => ({ dispose: () => { } }); + onWillExecuteCommand = () => Disposable.None; public executeCommand(commandId: string, ...args: any[]): TPromise { this.callCount++; diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 146bafc26a3..101a0f69d96 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -8,13 +8,13 @@ import URI from 'vs/base/common/uri'; import { Event, Emitter, debounceEvent, anyEvent } from 'vs/base/common/event'; import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations'; import { TernarySearchTree } from 'vs/base/common/map'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { isThenable } from 'vs/base/common/async'; import { LinkedList } from 'vs/base/common/linkedList'; import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { IdGenerator } from 'vs/base/common/idGenerator'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -174,7 +174,7 @@ class DecorationStyles { }); } - cleanUp(iter: IIterator): void { + cleanUp(iter: Iterator): void { // remove every rule for which no more // decoration (data) is kept. this isn't cheap let usedDecorations = new Set(); @@ -402,15 +402,13 @@ export class FileDecorationsService implements IDecorationsService { affectsResource() { return true; } }); - return { - dispose: () => { - // fire event that says 'yes' for any resource - // known to this provider. then dispose and remove it. - remove(); - this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) }); - wrapper.dispose(); - } - }; + return toDisposable(() => { + // fire event that says 'yes' for any resource + // known to this provider. then dispose and remove it. + remove(); + this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) }); + wrapper.dispose(); + }); } getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration { diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 610c5ef1946..c2c1ee52f58 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -13,6 +13,7 @@ import { isLinux, isWindows } from 'vs/base/common/platform'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; +import { ILogService } from 'vs/platform/log/common/log'; interface IMassagedMessageBoxOptions { @@ -34,10 +35,13 @@ export class DialogService implements IDialogService { _serviceBrand: any; constructor( - @IWindowService private windowService: IWindowService + @IWindowService private windowService: IWindowService, + @ILogService private logService: ILogService ) { } confirm(confirmation: IConfirmation): TPromise { + this.logService.trace('DialogService#confirm', confirmation.message); + const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation)); return this.windowService.showMessageBox(options).then(result => { @@ -86,6 +90,8 @@ export class DialogService implements IDialogService { } show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise { + this.logService.trace('DialogService#show', message); + const { options, buttonIndexMap } = this.massageMessageBoxOptions({ message, buttons, diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index c21f0dd7858..3c291c70b46 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -220,6 +220,16 @@ const schema: IJSONSchema = { description: nls.localize('vscode.extension.activationEvents.workspaceContains', 'An activation event emitted whenever a folder is opened that contains at least a file matching the specified glob pattern.'), body: 'workspaceContains:${4:filePattern}' }, + { + label: 'onFileSystem', + description: nls.localize('vscode.extension.activationEvents.onFileSystem', 'An activation event emitted whenever a file or folder is accessed with the given scheme.'), + body: 'onFileSystem:${1:scheme}' + }, + { + label: 'onSearch', + description: nls.localize('vscode.extension.activationEvents.onSearch', 'An activation event emitted whenever a search is started in the folder with the given scheme.'), + body: 'onSearch:${7:scheme}' + }, { label: 'onView', body: 'onView:${5:viewId}', @@ -288,6 +298,15 @@ const schema: IJSONSchema = { pattern: EXTENSION_IDENTIFIER_PATTERN } }, + extensionPack: { + description: nls.localize('vscode.extension.contributes.extensionPack', "A set of extensions that can be installed together. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), + type: 'array', + uniqueItems: true, + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN + } + }, scripts: { type: 'object', properties: { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index c7250d5cc51..0b6dd68de6e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -31,7 +31,7 @@ import { ICrashReporterService } from 'vs/workbench/services/crashReporter/elect import { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-browser/broadcastService'; import { isEqual } from 'vs/base/common/paths'; import { EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console'; import { getScopes } from 'vs/platform/configuration/common/configurationRegistry'; import { ILogService } from 'vs/platform/log/common/log'; @@ -96,11 +96,9 @@ export class ExtensionHostProcessWorker { const globalExitListener = () => this.terminate(); process.once('exit', globalExitListener); - this._toDispose.push({ - dispose: () => { - process.removeListener('exit', globalExitListener); - } - }); + this._toDispose.push(toDisposable(() => { + process.removeListener('exit', globalExitListener); + })); } public dispose(): void { @@ -369,7 +367,6 @@ export class ExtensionHostProcessWorker { isExtensionDevelopmentDebug: this._isExtensionDevDebug, appRoot: this._environmentService.appRoot, appSettingsHome: this._environmentService.appSettingsHome, - disableExtensions: this._environmentService.disableExtensions, extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath, extensionTestsPath: this._environmentService.extensionTestsPath }, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index c2f0c78bfe2..45c442c325b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -281,8 +281,8 @@ export class ExtensionService extends Disposable implements IExtensionService { this.startDelayed(lifecycleService); - if (this._environmentService.disableExtensions) { - this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All extensions are temporarily disabled. Reload the window to return to the previous state."), [{ + if (this._extensionEnablementService.allUserExtensionsDisabled) { + this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{ label: nls.localize('Reload', "Reload"), run: () => { this._windowService.reloadWindow(); @@ -513,7 +513,7 @@ export class ExtensionService extends Disposable implements IExtensionService { this._logOrShowMessage(severity, this._isDev ? messageWithSource(source, message) : message); }); - return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, log) + return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log) .then(({ system, user, development }) => { let result: { [extensionId: string]: IExtensionDescription; } = {}; system.forEach((systemExtension) => { @@ -536,7 +536,7 @@ export class ExtensionService extends Disposable implements IExtensionService { }); } - private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): TPromise { + private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise { return this._extensionEnablementService.getDisabledExtensions() .then(disabledExtensions => { @@ -759,7 +759,7 @@ export class ExtensionService extends Disposable implements IExtensionService { return result; } - private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> { + private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, extensionEnablementService: IExtensionEnablementService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> { const translationConfig: TPromise = platform.translationsConfigFile ? pfs.readFile(platform.translationsConfigFile, 'utf8').then((content) => { @@ -831,7 +831,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } const userExtensions = ( - environmentService.disableExtensions || !environmentService.extensionsPath + extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath ? TPromise.as([]) : this._scanExtensionsWithCache( windowService, diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 90cfe42920d..802d0be085c 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -615,4 +615,4 @@ export class ExtensionScanner { return []; }); } -} +} \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts index 26295cbc4a9..3672cdd0775 100644 --- a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts @@ -12,6 +12,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { normalize } from 'path'; import { rtrim, endsWith } from 'vs/base/common/strings'; import { sep } from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; export class FileWatcher { private isDisposed: boolean; @@ -26,6 +27,9 @@ export class FileWatcher { } public startWatching(): () => void { + if (this.contextService.getWorkspace().folders[0].uri.scheme !== Schemas.file) { + return () => { }; + } let basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath); if (basePath && basePath.indexOf('\\\\') === 0 && endsWith(basePath, sep)) { diff --git a/src/vs/workbench/services/group/common/editorGroupsService.ts b/src/vs/workbench/services/group/common/editorGroupsService.ts index dc1595f47d1..6bc438e07a4 100644 --- a/src/vs/workbench/services/group/common/editorGroupsService.ts +++ b/src/vs/workbench/services/group/common/editorGroupsService.ts @@ -476,4 +476,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/keybinding/common/keybindingIO.ts b/src/vs/workbench/services/keybinding/common/keybindingIO.ts index 13f82fbc817..80635e5d766 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingIO.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingIO.ts @@ -25,12 +25,12 @@ export class KeybindingIO { let quotedSerializedKeybinding = JSON.stringify(item.resolvedKeybinding.getUserSettingsLabel()); out.write(`{ "key": ${rightPaddedString(quotedSerializedKeybinding + ',', 25)} "command": `); - let serializedWhen = item.when ? item.when.serialize() : ''; + let quotedSerializedWhen = item.when ? JSON.stringify(item.when.serialize()) : ''; let quotedSerializeCommand = JSON.stringify(item.command); - if (serializedWhen.length > 0) { + if (quotedSerializedWhen.length > 0) { out.write(`${quotedSerializeCommand},`); out.writeLine(); - out.write(` "when": "${serializedWhen}" `); + out.write(` "when": ${quotedSerializedWhen} `); } else { out.write(`${quotedSerializeCommand} `); } diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 71af610716f..92fba70ec97 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -16,7 +16,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService, TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextFileService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService, TestEditorGroupsService, TestEditorService, TestLogService } from 'vs/workbench/test/workbenchTestServices'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -47,6 +47,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { mkdirp } from 'vs/base/node/pfs'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ILogService } from 'vs/platform/log/common/log'; interface Modifiers { metaKey?: boolean; @@ -68,7 +69,7 @@ suite('KeybindingsEditing', () => { instantiationService = new TestInstantiationService(); - instantiationService.stub(IEnvironmentService, { appKeybindingsPath: keybindingsFile }); + instantiationService.stub(IEnvironmentService, { appKeybindingsPath: keybindingsFile, appSettingsPath: path.join(testDir, 'settings.json') }); instantiationService.stub(IConfigurationService, ConfigurationService); instantiationService.stub(IConfigurationService, 'getValue', { 'eol': '\n' }); instantiationService.stub(IConfigurationService, 'onDidUpdateConfiguration', () => { }); @@ -82,6 +83,7 @@ suite('KeybindingsEditing', () => { instantiationService.stub(IEditorService, new TestEditorService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModeService, ModeServiceImpl); + instantiationService.stub(ILogService, new TestLogService()); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); instantiationService.stub(IFileService, new FileService( new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 8c649215708..93be98d9f2b 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -48,6 +48,7 @@ export interface ISetting { // TODO@roblou maybe need new type and new EditorModel for GUI editor instead of ISetting which is used for text settings editor type?: string | string[]; enum?: string[]; + enumDescriptions?: string[]; } export interface IExtensionSetting extends ISetting { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 4f3ca88dfe0..62a5d8c1997 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -473,7 +473,8 @@ export class DefaultSettings extends Disposable { valueRange: null, overrides: [], type: setting.type, - enum: setting.enum + enum: setting.enum, + enumDescriptions: setting.enumDescriptions }; } return null; @@ -492,7 +493,8 @@ export class DefaultSettings extends Disposable { }; } - private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup): ISettingsGroup[] { + private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup, seenSettings?: { [key: string]: boolean }): ISettingsGroup[] { + seenSettings = seenSettings ? seenSettings : {}; let title = config.title; if (!title) { const configWithTitleAndSameId = configurations.filter(c => c.id === config.id && c.title)[0]; @@ -516,14 +518,20 @@ export class DefaultSettings extends Disposable { settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null, contributedByExtension: !!config.contributedByExtension }; result.push(settingsGroup); } - const configurationSettings: ISetting[] = [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)]; + const configurationSettings: ISetting[] = []; + for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)]) { + if (!seenSettings[setting.key]) { + configurationSettings.push(setting); + seenSettings[setting.key] = true; + } + } if (configurationSettings.length) { configurationSettings.sort((a, b) => a.key.localeCompare(b.key)); settingsGroup.sections[settingsGroup.sections.length - 1].settings = configurationSettings; } } if (config.allOf) { - config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup)); + config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup, seenSettings)); } return result; } @@ -547,7 +555,7 @@ export class DefaultSettings extends Disposable { const value = prop.default; const description = (prop.description || '').split('\n'); const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : []; - result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides, type: prop.type, enum: prop.enum }); + result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides, type: prop.type, enum: prop.enum, enumDescriptions: prop.enumDescriptions }); } } return result; @@ -841,12 +849,8 @@ class SettingsContentBuilder { private pushSetting(setting: ISetting, indent: string): void { const settingStart = this.lineCountWithOffset + 1; - setting.descriptionRanges = []; - const descriptionPreValue = indent + '// '; - for (const line of setting.description) { - this._contentByLines.push(descriptionPreValue + line); - setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); - } + + this.pushSettingDescription(setting, indent); let preValueConent = indent; const keyString = JSON.stringify(setting.key); @@ -863,6 +867,32 @@ class SettingsContentBuilder { setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }; } + private pushSettingDescription(setting: ISetting, indent: string): void { + const fixSettingLink = line => line.replace(/`#(.*)#`/g, (match, settingName) => `\`${settingName}\``); + + setting.descriptionRanges = []; + const descriptionPreValue = indent + '// '; + for (let line of setting.description) { + // Remove setting link tag + line = fixSettingLink(line); + + this._contentByLines.push(descriptionPreValue + line); + setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); + } + + if (setting.enumDescriptions && setting.enumDescriptions.some(desc => !!desc)) { + setting.enumDescriptions.forEach((desc, i) => { + const line = desc ? + `${setting.enum[i]}: ${fixSettingLink(desc)}` : + setting.enum[i]; + + this._contentByLines.push(` // - ${line}`); + + setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); + }); + } + } + private pushValue(setting: ISetting, preValueConent: string, indent: string): void { let valueString = JSON.stringify(setting.value, null, indent); if (valueString && (typeof setting.value === 'object')) { diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 1182c79e506..b1683441fd7 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'; @@ -206,7 +206,6 @@ export class FileWalker { const useRipgrep = this.useRipgrep; let noSiblingsClauses: boolean; - let filePatternSeen = false; if (useRipgrep) { const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder).expression); cmd = ripgrep.cmd; @@ -265,9 +264,6 @@ export class FileWalker { if (useRipgrep && noSiblingsClauses) { for (const relativePath of relativeFiles) { - if (relativePath === this.filePattern) { - filePatternSeen = true; - } const basename = path.basename(relativePath); this.matchFile(onResult, { base: rootFolder, relativePath, basename }); if (this.isLimitHit) { @@ -276,22 +272,9 @@ export class FileWalker { } } if (last || this.isLimitHit) { - if (!filePatternSeen) { - this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => { - if (match) { - this.resultCount++; - onResult({ - base: folderQuery.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - }); - } - done(); - }); - } else { - done(); - } + done(); } + return; } @@ -384,17 +367,22 @@ export class FileWalker { cb(err, stdout, last); }; + let gotData = false; if (cmd.stdout) { // Should be non-null, but #38195 this.forwardData(cmd.stdout, encoding, onData); + cmd.stdout.once('data', () => gotData = true); } else { onMessage({ message: 'stdout is null' }); } - const stderr = this.collectData(cmd.stderr); - - let gotData = false; - cmd.stdout.once('data', () => gotData = true); + let stderr: Buffer[]; + if (cmd.stderr) { + // Should be non-null, but #38195 + stderr = this.collectData(cmd.stderr); + } else { + onMessage({ message: 'stderr is null' }); + } cmd.on('error', (err: Error) => { onData(err); @@ -474,6 +462,7 @@ export class FileWalker { const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { self.directoriesWalked++; + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -482,7 +471,7 @@ export class FileWalker { // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) { + if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -513,25 +502,11 @@ export class FileWalker { return done(); } - // Support relative paths to files from a root resource (ignores excludes) - return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => { - if (this.isCanceled || this.isLimitHit) { - return done(); - } + if (this.isCanceled || this.isLimitHit) { + return done(); + } - // Report result from file pattern if matching - if (match) { - this.resultCount++; - onResult({ - base: folderQuery.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - size - }); - } - - return this.doWalk(folderQuery, '', files, onResult, done); - }); + return this.doWalk(folderQuery, '', files, onResult, done); }); } @@ -551,22 +526,11 @@ export class FileWalker { }; } - private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void { - if (!this.filePattern || path.isAbsolute(this.filePattern)) { - return clb(null); - } - - const absolutePath = path.join(basePath, this.filePattern); - - return fs.stat(absolutePath, (error, stat) => { - return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files - }); - } - private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void { const rootFolder = folderQuery.folder; // Execute tasks on each file in parallel to optimize throughput + const hasSibling = glob.hasSiblingFn(() => files); flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => { // Check canceled @@ -574,17 +538,12 @@ export class FileWalker { return clb(null, undefined); } + // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - let siblings = files; - if (this.config.filePattern === file) { - siblings = []; - } - - // Check exclude pattern let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file; - if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) { + if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) { return clb(null, undefined); } @@ -721,9 +680,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() }); @@ -770,9 +730,9 @@ class AbsoluteAndRelativeParsedExpression { this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true }); } - public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise): string | TPromise { - return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) || - (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn)); + public test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): string | TPromise { + return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) || + (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling)); } public getBasenameTerms(): string[] { diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index fd40541b99c..f2281f3d5c7 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({ + onFirstListenerDidAdd: () => { + promise = this.doFileSearch(FileSearchEngine, config, p => emitter.fire(p), batchSize) + .then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } })); + }, + 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({ + onFirstListenerDidAdd: () => { + 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: { message: err.message, stack: err.stack } })); + }, + 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..c9e4a7f498c 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 }); @@ -78,7 +79,7 @@ export class RipgrepEngine { this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles); this.ripgrepParser.on('result', (match: ISerializedFileMatch) => { if (this.postProcessExclusions) { - const handleResultP = (>this.postProcessExclusions(match.path, undefined, () => getSiblings(match.path))) + const handleResultP = (>this.postProcessExclusions(match.path, undefined, glob.hasSiblingPromiseFn(() => getSiblings(match.path)))) .then(globMatch => { if (!globMatch) { onResult(match); @@ -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..3bb3cc19b4d 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,40 @@ 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: { + message: string, + stack: string + }; +} + +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 ffa1d267bb9..3450ab3e2fd 100644 --- a/src/vs/workbench/services/search/node/searchIpc.ts +++ b/src/vs/workbench/services/search/node/searchIpc.ts @@ -5,16 +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; } @@ -22,38 +22,38 @@ export class SearchChannel implements ISearchChannel { constructor(private service: IRawSearchService) { } - listen(event: string, arg?: any): Event { - throw new Error('No events'); + 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); + } + throw new Error('Event not found'); } call(command: string, arg?: any): TPromise { switch (command) { - 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('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 cc292c17701..7053106bc18 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -15,17 +15,18 @@ 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'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { onUnexpectedError } from 'vs/base/common/errors'; 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; @@ -54,18 +55,16 @@ export class SearchService implements ISearchService { this.searchProviders.push(provider); } - return { - dispose: () => { - if (scheme === 'file') { - this.fileSearchProvider = null; - } else { - const idx = this.searchProviders.indexOf(provider); - if (idx >= 0) { - this.searchProviders.splice(idx, 1); - } + return toDisposable(() => { + if (scheme === 'file') { + this.fileSearchProvider = null; + } else { + const idx = this.searchProviders.indexOf(provider); + if (idx >= 0) { + this.searchProviders.splice(idx, 1); } } - }; + }); } public extendQuery(query: ISearchQuery): void { @@ -90,18 +89,19 @@ export class SearchService implements ISearchService { } } - public search(query: ISearchQuery): PPromise { + public search(query: ISearchQuery, onProgress?: (item: ISearchProgressItem) => void): TPromise { this.forwardTelemetry(); let combinedPromise: TPromise; - return new PPromise((onComplete, onError, onProgress) => { + return new TPromise((onComplete, onError) => { // Get local results from dirty/untitled const localResults = this.getLocalResults(query); - // Allow caller to register progress callback - process.nextTick(() => localResults.values().filter((res) => !!res).forEach(onProgress)); + if (onProgress) { + localResults.values().filter((res) => !!res).forEach(onProgress); + } this.logService.trace('SearchService#search', JSON.stringify(query)); @@ -111,10 +111,10 @@ export class SearchService implements ISearchService { progress => { if (progress.resource) { // Match - if (!localResults.has(progress.resource)) { // don't override local results + if (!localResults.has(progress.resource) && onProgress) { // don't override local results onProgress(progress); } - } else { + } else if (onProgress) { // Progress onProgress(progress); } @@ -124,7 +124,10 @@ export class SearchService implements ISearchService { } }); - const providerPromise = this.extensionService.whenInstalledExtensionsRegistered().then(() => { + const schemesInQuery = query.folderQueries.map(fq => fq.folder.scheme); + const providerActivations = schemesInQuery.map(scheme => this.extensionService.activateByEvent(`onSearch:${scheme}`)); + + const providerPromise = TPromise.join(providerActivations).then(() => { // TODO@roblou this is not properly waiting for search-rg to finish registering itself // If no search provider has been registered for the 'file' schema, fall back on DiskSearch const providers = [ @@ -320,14 +323,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); }); } @@ -372,7 +375,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) => { @@ -420,6 +444,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/test/node/search.test.ts b/src/vs/workbench/services/search/test/node/search.test.ts index 1d19c3d0c76..fda8eed27b0 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -600,29 +600,6 @@ suite('FileSearchEngine', () => { }); }); - test('Files: relative path to file ignores excludes', function (done: () => void) { - this.timeout(testTimeout); - let engine = new FileSearchEngine({ - folderQueries: ROOT_FOLDER_QUERY, - filePattern: path.normalize(path.join('examples', 'company.js')), - excludePattern: { '**/*.js': true } - }); - - let count = 0; - let res: IRawFileMatch; - engine.search((result) => { - if (result) { - count++; - } - res = result; - }, () => { }, (error) => { - assert.ok(!error); - assert.equal(count, 1); - assert.equal(path.basename(res.relativePath), 'company.js'); - done(); - }); - }); - test('Files: Include pattern, single files', function (done: () => void) { this.timeout(testTimeout); let engine = new FileSearchEngine({ 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 f9e41489ea9..dbf27ba83f8 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -12,12 +12,11 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { guessMimeTypes } from 'vs/base/common/mime'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import URI from 'vs/base/common/uri'; -import * as diagnostics from 'vs/base/common/diagnostics'; -import * as types from 'vs/base/common/types'; +import { isUndefinedOrNull } from 'vs/base/common/types'; import { IMode } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -33,6 +32,8 @@ 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'; +import { ILogService } from 'vs/platform/log/common/log'; +import { isEqual, isEqualOrParent, hasToIgnoreCase } from 'vs/base/common/resources'; /** * 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. @@ -88,7 +89,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IBackupFileService private backupFileService: IBackupFileService, @IEnvironmentService private environmentService: IEnvironmentService, @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IHashService private hashService: IHashService + @IHashService private hashService: IHashService, + @ILogService private logService: ILogService ) { super(modelService, modeService); @@ -235,13 +237,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } load(options?: ILoadOptions): TPromise { - diag('load() - enter', this.resource, new Date()); + this.logService.trace('load() - enter', this.resource); // 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()); + this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource); return TPromise.as(this); } @@ -275,7 +277,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil isReadonly: false }; - return this.loadWithContent(content, backup); + return this.loadWithContent(content, options, backup); } // Otherwise load from file @@ -313,7 +315,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Guard against the model having changed in the meantime if (currentVersionId === this.versionId) { - return this.loadWithContent(content); + return this.loadWithContent(content, options); } return this; @@ -346,7 +348,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil }); } - private loadWithContent(content: IRawTextContent, backup?: URI): TPromise { + private loadWithContent(content: IRawTextContent, options?: ILoadOptions, backup?: URI): TPromise { return this.doLoadWithContent(content, backup).then(model => { // Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype @@ -360,10 +362,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil "fileGet" : { "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ - this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: path.extname(this.resource.fsPath), path: this.hashService.createSHA1(this.resource.fsPath) }); + this.telemetryService.publicLog('fileGet', { + mimeType: guessMimeTypes(this.resource.fsPath).join(', '), + ext: path.extname(this.resource.fsPath), + path: this.hashService.createSHA1(this.resource.fsPath), + reason: options && options.reason ? options.reason : LoadReason.OTHER + }); } return model; @@ -371,7 +379,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doLoadWithContent(content: IRawTextContent, backup?: URI): TPromise { - diag('load() - resolved content', this.resource, new Date()); + this.logService.trace('load() - resolved content', this.resource); // Update our resolved disk stat model this.updateLastResolvedDiskStat({ @@ -403,7 +411,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Join an existing request to create the editor model to avoid race conditions else if (this.createTextEditorModelPromise) { - diag('load() - join existing text editor model promise', this.resource, new Date()); + this.logService.trace('load() - join existing text editor model promise', this.resource); return this.createTextEditorModelPromise; } @@ -413,7 +421,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): TPromise { - diag('load() - updated text editor model', this.resource, new Date()); + this.logService.trace('load() - updated text editor model', this.resource); // Ensure we are not tracking a stale state this.setDirty(false); @@ -433,7 +441,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI): TPromise { - diag('load() - created text editor model', this.resource, new Date()); + this.logService.trace('load() - created text editor model', this.resource); this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => { const hasBackupContent = !!backupContent; @@ -493,11 +501,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private onModelContentChanged(): void { - diag(`onModelContentChanged() - enter`, this.resource, new Date()); + this.logService.trace(`onModelContentChanged() - enter`, this.resource); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date()); + this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource); // Ignore if blocking model changes if (this.blockModelContentChange) { @@ -509,7 +517,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Note: we currently only do this check when auto-save is turned off because there you see // a dirty indicator that you want to get rid of when undoing to the saved version. if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) { - diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date()); + this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource); // Clear flags const wasDirty = this.dirty; @@ -523,7 +531,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } - diag('onModelContentChanged() - model content changed and marked as dirty', this.resource, new Date()); + this.logService.trace('onModelContentChanged() - model content changed and marked as dirty', this.resource); // Mark as dirty this.makeDirty(); @@ -533,7 +541,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (!this.inConflictMode) { this.doAutoSave(this.versionId); } else { - diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date()); + this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource); } } @@ -554,7 +562,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doAutoSave(versionId: number): void { - diag(`doAutoSave() - enter for versionId ${versionId}`, this.resource, new Date()); + this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource); // Cancel any currently running auto saves to make this the one that succeeds this.cancelPendingAutoSave(); @@ -583,7 +591,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return TPromise.wrap(null); } - diag('save() - enter', this.resource, new Date()); + this.logService.trace('save() - enter', this.resource); // Cancel any currently running auto saves to make this the one that succeeds this.cancelPendingAutoSave(); @@ -592,11 +600,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doSave(versionId: number, options: ISaveOptions): TPromise { - if (types.isUndefinedOrNull(options.reason)) { + if (isUndefinedOrNull(options.reason)) { options.reason = SaveReason.EXPLICIT; } - diag(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource); // Lookup any running pending save for this versionId and return it if found // @@ -604,7 +612,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the save was not yet finished to disk // if (this.saveSequentializer.hasPendingSave(versionId)) { - diag(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource); return this.saveSequentializer.pendingSave; } @@ -617,7 +625,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Thus we avoid spawning multiple auto saves and only take the latest. // if ((!options.force && !this.dirty) || versionId !== this.versionId) { - diag(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource); return TPromise.wrap(null); } @@ -631,7 +639,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the first save has not returned yet. // if (this.saveSequentializer.hasPendingSave()) { - diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource); // Register this as the next upcoming save and return return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options)); @@ -697,7 +705,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) - diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - before updateContent()`, this.resource); return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, @@ -706,7 +714,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil etag: this.lastResolvedDiskStat.etag, writeElevated: options.writeElevated }).then(stat => { - diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - after updateContent()`, this.resource); // Telemetry if (this.isSettingsFile()) { @@ -718,18 +726,23 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil /* __GDPR__ "filePUT" : { "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ - this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: path.extname(this.lastResolvedDiskStat.resource.fsPath) }); + this.telemetryService.publicLog('filePUT', { + mimeType: guessMimeTypes(this.resource.fsPath).join(', '), + ext: path.extname(this.resource.fsPath), + reason: options.reason + }); } // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { - diag(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource); this.setDirty(false); } else { - diag(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource); } // Updated resolved stat with updated stat @@ -741,7 +754,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Emit File Saved Event this._onDidStateChange.fire(StateChange.SAVED); }, error => { - diag(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource, new Date()); + this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource); // Flag as error state in the model this.inErrorMode = true; @@ -761,15 +774,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private isSettingsFile(): boolean { + if (path.extname(this.resource.fsPath) !== '.json') { + return false; + } // Check for global settings file - if (path.isEqual(this.resource.fsPath, this.environmentService.appSettingsPath, !isLinux)) { + if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) { return true; } // Check for workspace settings file return this.contextService.getWorkspace().folders.some(folder => { - return path.isEqualOrParent(this.resource.fsPath, path.join(folder.uri.fsPath, '.vscode')); + return isEqualOrParent(this.resource, folder.toResource('.vscode'), hasToIgnoreCase(this.resource)); }); } @@ -940,7 +956,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } isResolved(): boolean { - return !types.isUndefinedOrNull(this.lastResolvedDiskStat); + return !isUndefinedOrNull(this.lastResolvedDiskStat); } isReadonly(): boolean { @@ -1072,11 +1088,3 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler { this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", path.basename(model.getResource().fsPath), toErrorMessage(error, false))); } } - -// Diagnostics support -let diag: (...args: any[]) => void; -if (!diag) { - diag = diagnostics.register('TextFileEditorModelDiagnostics', function (...args: any[]) { - console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])'); - }); -} diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 32e0164ecae..c8fbce69d0a 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; @@ -132,11 +132,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE return pendingLoad; } - let modelLoadOptions: ILoadOptions; - if (options && options.allowBinary) { - modelLoadOptions = { allowBinary: true }; - } - let modelPromise: TPromise; // Model exists @@ -152,7 +147,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // sync reload: do not return until model reloaded else { - modelPromise = model.load(modelLoadOptions); + modelPromise = model.load(options); } } else { modelPromise = TPromise.as(model); @@ -162,7 +157,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Model does not exist else { model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0); - modelPromise = model.load(modelLoadOptions); + modelPromise = model.load(options); // Install state change listener this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 737fbaed9c4..132e8b15c47 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -123,6 +123,12 @@ export enum SaveReason { WINDOW_CHANGE = 4 } +export enum LoadReason { + EDITOR = 1, + REFERENCE = 2, + OTHER = 3 +} + export const ITextFileService = createDecorator(TEXT_FILE_SERVICE_ID); export interface IRawTextContent extends IBaseStat { @@ -140,6 +146,10 @@ export interface IRawTextContent extends IBaseStat { export interface IModelLoadOrCreateOptions { + /** + * Context why the model is being loaded or created. + */ + reason?: LoadReason; /** * The encoding to use when resolving the model text content. @@ -210,6 +220,11 @@ export interface ILoadOptions { * Allow to load a model even if we think it is a binary file. */ allowBinary?: boolean; + + /** + * Context why the model is being loaded. + */ + reason?: LoadReason; } export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport { diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 792ade9778b..c7e655da6e1 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -12,7 +12,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as network from 'vs/base/common/network'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -34,7 +34,7 @@ class ResourceModelCollection extends ReferenceCollection { const resource = URI.parse(key); if (this.fileService.canHandleResource(resource)) { - return this.textFileService.models.loadOrCreate(resource); + return this.textFileService.models.loadOrCreate(resource, { reason: LoadReason.REFERENCE }); } return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index 920be3ef667..f33eac3aae8 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -33,6 +33,11 @@ export interface IWorkspaceEditingService { */ updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise; + /** + * enters the workspace with the provided path. + */ + enterWorkspace(path: string): TPromise; + /** * creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index a8b690fc006..6caf2e400a4 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -26,7 +26,7 @@ import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileS import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct } from 'vs/base/common/arrays'; import { isLinux } from 'vs/base/common/platform'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, hasToIgnoreCase } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; export class WorkspaceEditingService implements IWorkspaceEditingService { @@ -138,12 +138,16 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { private includesSingleFolderWorkspace(folders: URI[]): boolean { if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { const workspaceFolder = this.contextService.getWorkspace().folders[0]; - return (folders.some(folder => isEqual(folder, workspaceFolder.uri, !isLinux))); + return (folders.some(folder => isEqual(folder, workspaceFolder.uri, hasToIgnoreCase(folder)))); } return false; } + enterWorkspace(path: string): TPromise { + return this.doEnterWorkspace(() => this.windowService.enterWorkspace(path)); + } + createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path)); } diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts new file mode 100644 index 00000000000..bb0721629f8 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * 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/breadcrumbsModel'; +import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; + + +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 })])); + const configService = new class extends TestConfigurationService { + getValue(...args: any[]) { + if (args[0] === 'breadcrumbs.filePath') { + return 'on'; + } + if (args[0] === 'breadcrumbs.symbolPath') { + return 'on'; + } + return super.getValue(...args); + } + }; + + test('only uri, inside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, workspaceService, configService); + 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, configService); + 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/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index ba37b81ff7d..ef672a49dd1 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 () { @@ -635,4 +667,20 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); }); + + test('"TypeError: e.onCancellationRequested is not a function" calling hover provider in Insiders #54174', function () { + + disposables.push(extHost.registerHoverProvider(defaultSelector, { + provideHover(): any { + return new types.Hover('fofofofo'); + } + })); + + return rpcProtocol.sync().then(() => { + return commands.executeCommand('vscode.executeHoverProvider', model.uri, new types.Position(1, 1)).then(value => { + assert.equal(value.length, 1); + assert.equal(value[0].contents.length, 1); + }); + }); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts index 0b454064c07..9e4e11c0471 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts @@ -7,9 +7,9 @@ import * as assert from 'assert'; import URI, { UriComponents } from 'vs/base/common/uri'; -import { DiagnosticCollection } from 'vs/workbench/api/node/extHostDiagnostics'; +import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/node/extHostTypes'; -import { MainThreadDiagnosticsShape } from 'vs/workbench/api/node/extHost.protocol'; +import { MainThreadDiagnosticsShape, IMainContext } from 'vs/workbench/api/node/extHost.protocol'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { Emitter, toPromise } from 'vs/base/common/event'; @@ -27,7 +27,7 @@ suite('ExtHostDiagnostics', () => { test('disposeCheck', function () { - const collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); collection.dispose(); collection.dispose(); // that's OK @@ -44,13 +44,13 @@ suite('ExtHostDiagnostics', () => { test('diagnostic collection, forEach, clear, has', function () { - let collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); assert.equal(collection.name, 'test'); collection.dispose(); assert.throws(() => collection.name); let c = 0; - collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); collection.forEach(() => c++); assert.equal(c, 0); @@ -87,7 +87,7 @@ suite('ExtHostDiagnostics', () => { }); test('diagnostic collection, immutable read', function () { - let collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); collection.set(URI.parse('foo:bar'), [ new Diagnostic(new Range(0, 0, 1, 1), 'message-1'), new Diagnostic(new Range(0, 0, 1, 1), 'message-2') @@ -112,7 +112,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set with dupliclated tuples', function () { - let collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); let uri = URI.parse('sc:hightower'); collection.set([ [uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]], @@ -163,7 +163,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set tuple overrides, #11547', function () { let lastEntries: [UriComponents, IMarkerData[]][]; - let collection = new DiagnosticCollection('test', 100, new class extends DiagnosticsShape { + let collection = new DiagnosticCollection('test', 'test', 100, new class extends DiagnosticsShape { $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); @@ -192,7 +192,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, tuples and undefined (small array), #15585', function () { - const collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); let uri = URI.parse('sc:hightower'); let uri2 = URI.parse('sc:nomad'); let diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); @@ -213,7 +213,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, tuples and undefined (large array), #15585', function () { - const collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); const tuples: [URI, Diagnostic[]][] = []; for (let i = 0; i < 500; i++) { @@ -237,7 +237,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostic capping', function () { let lastEntries: [UriComponents, IMarkerData[]][]; - let collection = new DiagnosticCollection('test', 250, new class extends DiagnosticsShape { + let collection = new DiagnosticCollection('test', 'test', 250, new class extends DiagnosticsShape { $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); @@ -263,7 +263,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostic eventing', async function () { let emitter = new Emitter<(string | URI)[]>(); - let collection = new DiagnosticCollection('ddd', 100, new DiagnosticsShape(), emitter); + let collection = new DiagnosticCollection('ddd', 'test', 100, new DiagnosticsShape(), emitter); let diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); let diag2 = new Diagnostic(new Range(1, 1, 2, 3), 'diag2'); @@ -301,7 +301,7 @@ suite('ExtHostDiagnostics', () => { test('vscode.languages.onDidChangeDiagnostics Does Not Provide Document URI #49582', async function () { let emitter = new Emitter<(string | URI)[]>(); - let collection = new DiagnosticCollection('ddd', 100, new DiagnosticsShape(), emitter); + let collection = new DiagnosticCollection('ddd', 'test', 100, new DiagnosticsShape(), emitter); let diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); @@ -324,7 +324,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics with related information', function (done) { - let collection = new DiagnosticCollection('ddd', 100, new class extends DiagnosticsShape { + let collection = new DiagnosticCollection('ddd', 'test', 100, new class extends DiagnosticsShape { $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) { let [[, data]] = entries; @@ -347,4 +347,33 @@ suite('ExtHostDiagnostics', () => { collection.set(URI.parse('aa:bb'), [diag]); }); + + test('vscode.languages.getDiagnostics appears to return old diagnostics in some circumstances #54359', function () { + const ownerHistory: string[] = []; + const diags = new ExtHostDiagnostics(new class implements IMainContext { + getProxy(id: any): any { + return new class DiagnosticsShape { + $clear(owner: string): void { + ownerHistory.push(owner); + } + }; + } + set(): any { + return null; + } + assertRegistered(): void { + + } + }); + + let collection1 = diags.createDiagnosticCollection('foo'); + let collection2 = diags.createDiagnosticCollection('foo'); // warns, uses a different owner + + collection1.clear(); + collection2.clear(); + + assert.equal(ownerHistory.length, 2); + assert.equal(ownerHistory[0], 'foo'); + assert.equal(ownerHistory[1], 'foo0'); + }); }); 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 b9995cf46f8..3b96a8f3c01 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -404,7 +404,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - getHover(model, new EditorPosition(1, 1)).then(value => { + getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); @@ -423,7 +423,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - getHover(model, new EditorPosition(1, 1)).then(value => { + getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 4, startColumn: 1, endLineNumber: 9, endColumn: 8 }); @@ -447,7 +447,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getHover(model, new EditorPosition(1, 1)).then(value => { + return getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 2); let [first, second] = value as Hover[]; assert.equal(first.contents[0].value, 'registered second'); @@ -472,7 +472,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - getHover(model, new EditorPosition(1, 1)).then(value => { + getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); 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 49d33da63b2..bad1d62b9d7 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -606,37 +606,6 @@ suite('ExtHostSearch', () => { assert.equal(cancels, 2, 'Expected all invocations to be canceled when hitting limit'); }); - test('respects filePattern', async () => { - const reportedResults = [ - joinPath(rootFolderA, 'file1.ts'), - joinPath(rootFolderA, 'file2.ts'), - joinPath(rootFolderA, 'file3.ts'), - ]; - - await registerTestSearchProvider({ - provideFileSearchResults(query: vscode.FileSearchQuery, 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: 'file3', - - folderQueries: [ - { - folder: rootFolderA - } - ] - }; - - const { results } = await runFileSearch(query); - assert.equal(results.length, 1); - compareURIs(results, reportedResults.slice(2)); - }); - test('works with non-file schemes', async () => { const reportedResults = [ joinPath(fancySchemeFolderA, 'file1.ts'), diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 9c15b408467..14d5a319045 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -10,6 +10,7 @@ import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEdi import { Promise, TPromise } from 'vs/base/common/winjs.base'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import * as paths from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; import URI from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -26,7 +27,7 @@ import { TextModelResolverService } from 'vs/workbench/services/textmodelResolve import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, Workspace } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; @@ -46,10 +47,9 @@ import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { isLinux } from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; @@ -74,6 +74,7 @@ import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { Dimension } from 'vs/base/browser/dom'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, void 0); @@ -84,7 +85,7 @@ export const TestEnvironmentService = new EnvironmentService(parseArgs(process.a export class TestContextService implements IWorkspaceContextService { public _serviceBrand: any; - private workspace: IWorkbenchWorkspace; + private workspace: Workspace; private options: any; private readonly _onDidChangeWorkspaceName: Emitter; @@ -131,7 +132,7 @@ export class TestContextService implements IWorkspaceContextService { } public getWorkspaceFolder(resource: URI): IWorkspaceFolder { - return this.isInsideWorkspace(resource) ? this.workspace.folders[0] : null; + return this.workspace.getFolder(resource); } public setWorkspace(workspace: any): void { @@ -148,7 +149,7 @@ export class TestContextService implements IWorkspaceContextService { public isInsideWorkspace(resource: URI): boolean { if (resource && this.workspace) { - return paths.isEqualOrParent(resource.fsPath, this.workspace.folders[0].uri.fsPath, !isLinux /* ignorecase */); + return resources.isEqualOrParent(resource, this.workspace.folders[0].uri, resources.hasToIgnoreCase(resource)); } return false; @@ -159,16 +160,7 @@ export class TestContextService implements IWorkspaceContextService { } public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { - return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier); - } - - private pathEquals(path1: string, path2: string): boolean { - if (!isLinux) { - path1 = path1.toLowerCase(); - path2 = path2.toLowerCase(); - } - - return path1 === path2; + return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && resources.isEqual(this.workspace.folders[0].uri, workspaceIdentifier, resources.hasToIgnoreCase(workspaceIdentifier)); } } @@ -275,6 +267,7 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(IEnvironmentService, TestEnvironmentService); instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(IHashService, new TestHashService()); + instantiationService.stub(ILogService, new TestLogService()); instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService([new TestEditorGroup(0)])); const editorService = new TestEditorService(); instantiationService.stub(IEditorService, editorService); @@ -283,6 +276,19 @@ export function workbenchInstantiationService(): IInstantiationService { return instantiationService; } +export class TestLogService implements ILogService { + _serviceBrand: any; onDidChangeLogLevel: Event; + getLevel(): LogLevel { return LogLevel.Info; } + setLevel(level: LogLevel): void { } + trace(message: string, ...args: any[]): void { } + debug(message: string, ...args: any[]): void { } + info(message: string, ...args: any[]): void { } + warn(message: string, ...args: any[]): void { } + error(message: string | Error, ...args: any[]): void { } + critical(message: string | Error, ...args: any[]): void { } + dispose(): void { } +} + export class TestDecorationsService implements IDecorationsService { _serviceBrand: any; onDidChangeDecorations: Event = Event.None; @@ -673,6 +679,7 @@ export class TestEditorGroup implements IEditorGroupView { dispose(): void { } toJSON(): object { return Object.create(null); } layout(width: number, height: number): void { } + relayout() { } } export class TestEditorService implements EditorServiceImpl { @@ -1008,6 +1015,10 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } + enterWorkspace(path: string): TPromise { + return TPromise.as(void 0); + } + createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return TPromise.as(void 0); } @@ -1044,7 +1055,7 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + openWindow(paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { return TPromise.as(void 0); } @@ -1161,6 +1172,10 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } + enterWorkspace(windowId: number, path: string): TPromise { + return TPromise.as(void 0); + } + createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return TPromise.as(void 0); } @@ -1242,7 +1257,7 @@ export class TestWindowsService implements IWindowsService { } // Global methods - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { return TPromise.as(void 0); } @@ -1254,7 +1269,7 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { return TPromise.as(void 0); } @@ -1294,6 +1309,10 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } + getActiveWindowId(): TPromise { + return TPromise.as(undefined); + } + // This needs to be handled from browser process to prevent // foreground ordering issues on Windows openExternal(url: string): TPromise { diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index fa632fcf4f3..2df703e1c1f 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -64,7 +64,6 @@ import 'vs/workbench/parts/scm/electron-browser/scmViewlet'; // can be packaged import 'vs/workbench/parts/debug/electron-browser/debug.contribution'; import 'vs/workbench/parts/debug/browser/debugQuickOpen'; import 'vs/workbench/parts/debug/electron-browser/repl'; -import 'vs/workbench/parts/debug/browser/debugEditorActions'; import 'vs/workbench/parts/debug/browser/debugViewlet'; // can be packaged separately import 'vs/workbench/parts/markers/electron-browser/markers.contribution'; @@ -142,4 +141,4 @@ import 'vs/workbench/parts/navigation/common/navigation.contribution'; // services import 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService'; -import 'vs/workbench/parts/experiments/electron-browser/experiments.contribution'; \ No newline at end of file +import 'vs/workbench/parts/experiments/electron-browser/experiments.contribution'; diff --git a/test/smoke/src/areas/workbench/viewlet.ts b/test/smoke/src/areas/workbench/viewlet.ts index 2293752b705..84ccb601012 100644 --- a/test/smoke/src/areas/workbench/viewlet.ts +++ b/test/smoke/src/areas/workbench/viewlet.ts @@ -12,6 +12,6 @@ export abstract class Viewlet { constructor(protected code: Code) { } async waitForTitle(fn: (title: string) => boolean): Promise { - await this.code.waitForTextContent('.monaco-workbench-container .part.sidebar > .title > .title-label > span', undefined, fn); + await this.code.waitForTextContent('.monaco-workbench .part.sidebar > .title > .title-label > h2', undefined, fn); } } \ No newline at end of file diff --git a/test/tree/server.js b/test/tree/server.js index 2a931b51727..b1105bfb741 100644 --- a/test/tree/server.js +++ b/test/tree/server.js @@ -23,7 +23,7 @@ async function getTree(fsPath, level) { const childNames = await fs.readdir(fsPath); const children = await Promise.all(childNames.map(async childName => await getTree(path.join(fsPath, childName), level + 1))); - return { element, collapsed: false, children }; + return { element, collapsible: true, collapsed: false, children }; } app.use(serve('public')); diff --git a/yarn.lock b/yarn.lock index 1ac2e5486b8..cb804128177 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5436,9 +5436,9 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" -spdlog@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.6.0.tgz#20632ed4f1558ffa46e8a5827a5e97c61e0fa9ed" +spdlog@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.7.1.tgz#960b8cdced12e8c482d1df9fa65220789f276979" dependencies: bindings "^1.3.0" mkdirp "^0.5.1" @@ -6256,9 +6256,9 @@ vscode-textmate@^4.0.1: dependencies: oniguruma "^7.0.0" -vscode-xterm@3.6.0-beta1: - version "3.6.0-beta1" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.6.0-beta1.tgz#6ce8b79d33f7506ac3571d92c15f9d65e8b856ca" +vscode-xterm@3.6.0-beta3: + version "3.6.0-beta3" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.6.0-beta3.tgz#fe383ff8df66603088e36c1f9ac987b0de68bd1b" vso-node-api@^6.1.2-preview: version "6.1.2-preview"