diff --git a/.github/commands.yml b/.github/commands.yml index c92792325293c..21ebd49ec3c29 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -4,7 +4,7 @@ { type: 'comment', name: 'question', - allowUsers: ['cleidigh', 'usernamehw'], + allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], action: 'updateLabels', addLabel: '*question' }, @@ -60,7 +60,7 @@ { type: 'comment', name: 'duplicate', - allowUsers: ['cleidigh', 'usernamehw'], + allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], action: 'updateLabels', addLabel: '*duplicate' }, @@ -74,7 +74,7 @@ { type: 'comment', name: 'confirm', - allowUsers: ['cleidigh', 'usernamehw'], + allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], action: 'updateLabels', addLabel: 'confirmed', removeLabel: 'confirmation-pending' @@ -90,14 +90,14 @@ { type: 'comment', name: 'findDuplicates', - allowUsers: ['cleidigh', 'usernamehw'], + allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], action: 'comment', comment: "Potential duplicates:\n${potentialDuplicates}" }, { type: 'comment', name: 'needsMoreInfo', - allowUsers: ['cleidigh', 'usernamehw'], + allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], 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/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index c088bb8b4577f..84764519f5325 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -107,6 +107,16 @@ steps: displayName: Run integration tests condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + cd test/smoke + yarn compile + cd - + yarn smoketest --web --headless + continueOnError: true + displayName: Run web smoke tests + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin.zip * && popd diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml new file mode 100644 index 0000000000000..39a8f23b26218 --- /dev/null +++ b/build/azure-pipelines/exploration-build.yml @@ -0,0 +1,40 @@ +pool: + vmImage: 'Ubuntu-16.04' + +trigger: + branches: + include: ['master'] +pr: + branches: + include: ['master'] + +steps: +- task: NodeTool@0 + inputs: + versionSpec: "10.15.1" + +- task: AzureKeyVault@1 + displayName: 'Azure Key Vault: Get Secrets' + inputs: + azureSubscription: 'vscode-builds-subscription' + KeyVaultName: vscode + +- script: | + set -e + + cat << EOF > ~/.netrc + machine github.com + login vscode + password $(github-distro-mixin-password) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + + git checkout origin/electron-6.0.x + git merge origin/master + + # Push master branch into exploration branch + git push origin HEAD:electron-6.0.x + + displayName: Sync & Merge Exploration diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 27467db5743ba..664ef4bba1823 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.38.2", + "version": "1.38.3", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index f6594804322e0..aaedf0a7bc82d 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -50,6 +50,7 @@ const indentationFilter = [ '!src/vs/css.js', '!src/vs/css.build.js', '!src/vs/loader.js', + '!src/vs/base/common/insane/insane.js', '!src/vs/base/common/marked/marked.js', '!src/vs/base/node/terminateProcess.sh', '!src/vs/base/node/cpuUsage.sh', @@ -131,24 +132,44 @@ const eslintFilter = [ '!src/vs/nls.js', '!src/vs/css.build.js', '!src/vs/nls.build.js', + '!src/**/insane.js', '!src/**/marked.js', '!**/test/**' ]; -const tslintFilter = [ - 'src/**/*.ts', - 'test/**/*.ts', - 'extensions/**/*.ts', +const tslintBaseFilter = [ '!**/fixtures/**', '!**/typings/**', '!**/node_modules/**', - '!extensions/typescript/test/colorize-fixtures/**', + '!extensions/typescript-basics/test/colorize-fixtures/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', '!extensions/**/*.test.ts', '!extensions/html-language-features/server/lib/jquery.d.ts' ]; +const tslintCoreFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + '!extensions/**/*.ts', + '!test/smoke/**', + ...tslintBaseFilter +]; + +const tslintExtensionsFilter = [ + 'extensions/**/*.ts', + '!src/**/*.ts', + '!test/**/*.ts', + ...tslintBaseFilter +]; + +const tslintHygieneFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + 'extensions/**/*.ts', + ...tslintBaseFilter +]; + const copyrightHeaderLines = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -165,12 +186,20 @@ gulp.task('eslint', () => { }); gulp.task('tslint', () => { - const options = { emitError: true }; - - return vfs.src(all, { base: '.', follow: true, allowEmpty: true }) - .pipe(filter(tslintFilter)) - .pipe(gulptslint.default({ rulesDirectory: 'build/lib/tslint' })) - .pipe(gulptslint.default.report(options)); + return es.merge([ + + // Core: include type information (required by certain rules like no-nodejs-globals) + vfs.src(all, { base: '.', follow: true, allowEmpty: true }) + .pipe(filter(tslintCoreFilter)) + .pipe(gulptslint.default({ rulesDirectory: 'build/lib/tslint', program: tslint.Linter.createProgram('src/tsconfig.json') })) + .pipe(gulptslint.default.report({ emitError: true })), + + // Exenstions: do not include type information + vfs.src(all, { base: '.', follow: true, allowEmpty: true }) + .pipe(filter(tslintExtensionsFilter)) + .pipe(gulptslint.default({ rulesDirectory: 'build/lib/tslint' })) + .pipe(gulptslint.default.report({ emitError: true })) + ]).pipe(es.through()); }); function hygiene(some) { @@ -283,7 +312,7 @@ function hygiene(some) { .pipe(copyrights); const typescript = result - .pipe(filter(tslintFilter)) + .pipe(filter(tslintHygieneFilter)) .pipe(formatting) .pipe(tsl); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 11da75e482ddf..8f63d7b7d0bf9 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -41,6 +41,9 @@ const vscodeWebResources = [ // Webview 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', + // Extension Worker + 'out-build/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js', + // Excludes '!out-build/vs/**/{node,electron-browser,electron-main}/**', '!out-build/vs/editor/standalone/**', @@ -53,6 +56,7 @@ const buildfile = require('../src/buildfile'); const vscodeWebEntryPoints = [ buildfile.workbenchWeb, buildfile.serviceWorker, + buildfile.workerExtensionHost, buildfile.keyboardMaps, buildfile.base ]; @@ -148,4 +152,4 @@ const dashed = (str) => (str ? `-${str}` : ``); vscodeWebTaskCI )); gulp.task(vscodeWebTask); -}); \ No newline at end of file +}); diff --git a/build/lib/tslint/abstractGlobalsRule.js b/build/lib/tslint/abstractGlobalsRule.js new file mode 100644 index 0000000000000..1566c0aa57605 --- /dev/null +++ b/build/lib/tslint/abstractGlobalsRule.js @@ -0,0 +1,45 @@ +"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 }); +const Lint = require("tslint"); +class AbstractGlobalsRuleWalker extends Lint.RuleWalker { + constructor(file, program, opts, _config) { + super(file, opts); + this.program = program; + this._config = _config; + } + visitIdentifier(node) { + if (this.getDisallowedGlobals().some(disallowedGlobal => disallowedGlobal === node.text)) { + if (this._config.allowed && this._config.allowed.some(allowed => allowed === node.text)) { + return; // override + } + const checker = this.program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (symbol) { + const declarations = symbol.declarations; + if (Array.isArray(declarations) && symbol.declarations.some(declaration => { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const sourceFile = parent.getSourceFile(); + if (sourceFile) { + const fileName = sourceFile.fileName; + if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { + return true; + } + } + } + } + return false; + })) { + this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); + } + } + } + super.visitIdentifier(node); + } +} +exports.AbstractGlobalsRuleWalker = AbstractGlobalsRuleWalker; diff --git a/build/lib/tslint/abstractGlobalsRule.ts b/build/lib/tslint/abstractGlobalsRule.ts new file mode 100644 index 0000000000000..543720455c3b7 --- /dev/null +++ b/build/lib/tslint/abstractGlobalsRule.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; +import * as Lint from 'tslint'; + +interface AbstractGlobalsRuleConfig { + target: string; + allowed: string[]; +} + +export abstract class AbstractGlobalsRuleWalker extends Lint.RuleWalker { + + constructor(file: ts.SourceFile, private program: ts.Program, opts: Lint.IOptions, private _config: AbstractGlobalsRuleConfig) { + super(file, opts); + } + + protected abstract getDisallowedGlobals(): string[]; + + protected abstract getDefinitionPattern(): string; + + visitIdentifier(node: ts.Identifier) { + if (this.getDisallowedGlobals().some(disallowedGlobal => disallowedGlobal === node.text)) { + if (this._config.allowed && this._config.allowed.some(allowed => allowed === node.text)) { + return; // override + } + + const checker = this.program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (symbol) { + const declarations = symbol.declarations; + if (Array.isArray(declarations) && symbol.declarations.some(declaration => { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const sourceFile = parent.getSourceFile(); + if (sourceFile) { + const fileName = sourceFile.fileName; + if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { + return true; + } + } + } + } + + return false; + })) { + this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); + } + } + } + + super.visitIdentifier(node); + } +} diff --git a/build/lib/tslint/noDomGlobalsRule.js b/build/lib/tslint/noDomGlobalsRule.js new file mode 100644 index 0000000000000..a83ac8f7f5958 --- /dev/null +++ b/build/lib/tslint/noDomGlobalsRule.js @@ -0,0 +1,34 @@ +"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 }); +const Lint = require("tslint"); +const minimatch = require("minimatch"); +const abstractGlobalsRule_1 = require("./abstractGlobalsRule"); +class Rule extends Lint.Rules.TypedRule { + applyWithProgram(sourceFile, program) { + const configs = this.getOptions().ruleArguments; + for (const config of configs) { + if (minimatch(sourceFile.fileName, config.target)) { + return this.applyWithWalker(new NoDOMGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); + } + } + return []; + } +} +exports.Rule = Rule; +class NoDOMGlobalsRuleWalker extends abstractGlobalsRule_1.AbstractGlobalsRuleWalker { + getDefinitionPattern() { + return 'lib.dom.d.ts'; + } + getDisallowedGlobals() { + // intentionally not complete + return [ + "window", + "document", + "HTMLElement" + ]; + } +} diff --git a/build/lib/tslint/noDomGlobalsRule.ts b/build/lib/tslint/noDomGlobalsRule.ts new file mode 100644 index 0000000000000..df9e67bf78b6f --- /dev/null +++ b/build/lib/tslint/noDomGlobalsRule.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import * as minimatch from 'minimatch'; +import { AbstractGlobalsRuleWalker } from './abstractGlobalsRule'; + +interface NoDOMGlobalsRuleConfig { + target: string; + allowed: string[]; +} + +export class Rule extends Lint.Rules.TypedRule { + + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + const configs = this.getOptions().ruleArguments; + + for (const config of configs) { + if (minimatch(sourceFile.fileName, config.target)) { + return this.applyWithWalker(new NoDOMGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); + } + } + + return []; + } +} + +class NoDOMGlobalsRuleWalker extends AbstractGlobalsRuleWalker { + + getDefinitionPattern(): string { + return 'lib.dom.d.ts'; + } + + getDisallowedGlobals(): string[] { + // intentionally not complete + return [ + "window", + "document", + "HTMLElement" + ]; + } +} diff --git a/build/lib/tslint/noNodejsGlobalsRule.js b/build/lib/tslint/noNodejsGlobalsRule.js new file mode 100644 index 0000000000000..8c36fa342c273 --- /dev/null +++ b/build/lib/tslint/noNodejsGlobalsRule.js @@ -0,0 +1,41 @@ +"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 }); +const Lint = require("tslint"); +const minimatch = require("minimatch"); +const abstractGlobalsRule_1 = require("./abstractGlobalsRule"); +class Rule extends Lint.Rules.TypedRule { + applyWithProgram(sourceFile, program) { + const configs = this.getOptions().ruleArguments; + for (const config of configs) { + if (minimatch(sourceFile.fileName, config.target)) { + return this.applyWithWalker(new NoNodejsGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); + } + } + return []; + } +} +exports.Rule = Rule; +class NoNodejsGlobalsRuleWalker extends abstractGlobalsRule_1.AbstractGlobalsRuleWalker { + getDefinitionPattern() { + return '@types/node'; + } + getDisallowedGlobals() { + // https://nodejs.org/api/globals.html#globals_global_objects + return [ + "NodeJS", + "Buffer", + "__dirname", + "__filename", + "clearImmediate", + "exports", + "global", + "module", + "process", + "setImmediate" + ]; + } +} diff --git a/build/lib/tslint/noNodejsGlobalsRule.ts b/build/lib/tslint/noNodejsGlobalsRule.ts new file mode 100644 index 0000000000000..7e5767d857086 --- /dev/null +++ b/build/lib/tslint/noNodejsGlobalsRule.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import * as minimatch from 'minimatch'; +import { AbstractGlobalsRuleWalker } from './abstractGlobalsRule'; + +interface NoNodejsGlobalsConfig { + target: string; + allowed: string[]; +} + +export class Rule extends Lint.Rules.TypedRule { + + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + const configs = this.getOptions().ruleArguments; + + for (const config of configs) { + if (minimatch(sourceFile.fileName, config.target)) { + return this.applyWithWalker(new NoNodejsGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); + } + } + + return []; + } +} + +class NoNodejsGlobalsRuleWalker extends AbstractGlobalsRuleWalker { + + getDefinitionPattern(): string { + return '@types/node'; + } + + getDisallowedGlobals(): string[] { + // https://nodejs.org/api/globals.html#globals_global_objects + return [ + "NodeJS", + "Buffer", + "__dirname", + "__filename", + "clearImmediate", + "exports", + "global", + "module", + "process", + "setImmediate" + ]; + } +} diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index cd41092192c8d..6b1159bc65cdd 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -78,4 +78,42 @@ const processTreeDts = path.join('node_modules', 'windows-process-tree', 'typing if (fs.existsSync(processTreeDts)) { console.log('Removing windows-process-tree.d.ts'); fs.unlinkSync(processTreeDts); -} \ No newline at end of file +} + +function getInstalledVersion(packageName, cwd) { + const opts = {}; + if (cwd) { + opts.cwd = cwd; + } + + const result = cp.spawnSync(yarn, ['list', '--pattern', packageName], opts); + const stdout = result.stdout.toString(); + const match = stdout.match(new RegExp(packageName + '@(\\S+)')); + if (!match || !match[1]) { + throw new Error('Unexpected output from yarn list: ' + stdout); + } + + return match[1]; +} + +function assertSameVersionsBetweenFolders(packageName, otherFolder) { + const baseVersion = getInstalledVersion(packageName); + const otherVersion = getInstalledVersion(packageName, otherFolder); + + if (baseVersion !== otherVersion) { + throw new Error(`Mismatched versions installed for ${packageName}: root has ${baseVersion}, ./${otherFolder} has ${otherVersion}. These should be the same!`); + } +} + +// Check that modules in both the base package.json and remote/ have the same version installed +const requireSameVersionsInRemote = [ + 'xterm', + 'xterm-addon-search', + 'xterm-addon-web-links', + 'node-pty', + 'vscode-ripgrep' +]; + +requireSameVersionsInRemote.forEach(packageName => { + assertSameVersionsBetweenFolders(packageName, 'remote'); +}); diff --git a/build/package.json b/build/package.json index a382150c3f004..7562988862047 100644 --- a/build/package.json +++ b/build/package.json @@ -42,7 +42,7 @@ "tslint": "^5.9.1", "typescript": "3.5.2", "vsce": "1.48.0", - "vscode-telemetry-extractor": "1.5.3", + "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" }, "scripts": { diff --git a/build/win32/code.iss b/build/win32/code.iss index 831b31a3c711c..ee70efb974d02 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -19,8 +19,8 @@ Compression=lzma SolidCompression=yes AppMutex={code:GetAppMutex} SetupMutex={#AppMutex}setup -WizardImageFile={#RepoDir}\resources\win32\inno-big.bmp -WizardSmallImageFile={#RepoDir}\resources\win32\inno-small.bmp +WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp" +WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp" SetupIconFile={#RepoDir}\resources\win32\code.ico UninstallDisplayIcon={app}\{#ExeBasename}.exe ChangesEnvironment=true diff --git a/build/yarn.lock b/build/yarn.lock index 23ac4d9b84da3..7e4f11d8c60ac 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2313,19 +2313,19 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-ripgrep@^1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.5.5.tgz#24c0e9cb356cf889c98e15ecb58f9cf654a1d961" - integrity sha512-OrPrAmcun4+uZAuNcQvE6CCPskh+5AsjANod/Q3zRcJcGNxgoOSGlQN9RPtatkUNmkN8Nn8mZBnS1jMylu/dKg== +vscode-ripgrep@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.5.6.tgz#93bf5c99ca5f8248950a305e224f6ca153c30af4" + integrity sha512-WRIM9XpUj6dsfdAmuI3ANbmT1ysPUVsYy/2uCLDHJa9kbiB4T7uGvFnnc0Rgx2qQnyRAwL7PeWaFgUljPPxf2g== -vscode-telemetry-extractor@1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.5.3.tgz#c17f9065a47425edafd23ea161e80c23274e009d" - integrity sha512-feioJ1e1KyMa9rzblnLbSOduo+Ny0l62H3/bSDgfgCSnU/km+tTSYxPBvZHVr7iQfQGC95J61yC/ObqS9EbaQg== +vscode-telemetry-extractor@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.5.4.tgz#bcb0d17667fa1b77715e3a3bf372ade18f846782" + integrity sha512-MN9LNPo0Rc6cy3sIWTAG97PTWkEKdRnP0VeYoS8vjKSNtG9CAsrUxHgFfYoHm2vNK/ijd0a4NzETyVGO2kT6hw== dependencies: command-line-args "^5.1.1" ts-morph "^3.1.3" - vscode-ripgrep "^1.5.5" + vscode-ripgrep "^1.5.6" vso-node-api@6.1.2-preview: version "6.1.2-preview" diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index e555d61457e95..3d1049af6b4d6 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -18,7 +18,7 @@ "watch": "gulp watch-extension:configuration-editing" }, "dependencies": { - "jsonc-parser": "2.0.2", + "jsonc-parser": "^2.1.1", "vscode-nls": "^4.0.0" }, "contributes": { diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index 55950a9021eba..c24857720e65d 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -129,6 +129,13 @@ "type": "string", "description": "The service you want to work on." }, + "runServices": { + "type": "array", + "description": "An array of services that should be started and stopped.", + "items": { + "type": "string" + } + }, "workspaceFolder": { "type": "string", "description": "The path of the workspace folder inside the container." @@ -178,4 +185,4 @@ "$ref": "#/definitions/devContainerCommon" } ] -} \ No newline at end of file +} diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index ce653f097a9e8..39ad23373148d 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.8.tgz#fe444203ecef1162348cd6deb76c62477b2cc6e9" integrity sha512-I4+DbJEhLEg4/vIy/2gkWDvXBOOtPKV9EnLhYjMoqxcRW+TTZtUftkHktz/a8suoD5mUL7m6ReLrkPvSsCQQmw== -jsonc-parser@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" - integrity sha512-TSU435K5tEKh3g7bam1AFf+uZrISheoDsLlpmAo6wWZYqjsnd09lHYK1Qo+moK4Ikifev1Gdpa69g4NELKnCrQ== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/cpp/cgmanifest.json b/extensions/cpp/cgmanifest.json index 8ab81224b2a08..b8eb057983851 100644 --- a/extensions/cpp/cgmanifest.json +++ b/extensions/cpp/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jeff-hykin/cpp-textmate-grammar", "repositoryUrl": "https://github.com/jeff-hykin/cpp-textmate-grammar", - "commitHash": "218448eb46260864352d569db13be6cb20767e92" + "commitHash": "031ef619bef4c5a1ca46e6fa69d7c913e0c32068" } }, "license": "MIT", - "version": "1.12.21", + "version": "1.13.2", "description": "The files syntaxes/c.json and syntaxes/c++.json were derived from https://github.com/atom/language-c which was originally converted from the C TextMate bundle https://github.com/textmate/c.tmbundle." }, { diff --git a/extensions/cpp/syntaxes/cpp.tmLanguage.json b/extensions/cpp/syntaxes/cpp.tmLanguage.json index 963a7c6cc283b..4517b91af426d 100644 --- a/extensions/cpp/syntaxes/cpp.tmLanguage.json +++ b/extensions/cpp/syntaxes/cpp.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/jeff-hykin/cpp-textmate-grammar/commit/74c2c0eaad8f647e98a188da0f95a64f7239cbe0", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/031ef619bef4c5a1ca46e6fa69d7c913e0c32068", "name": "C++", "scopeName": "source.cpp", "patterns": [ @@ -121,6 +121,250 @@ "match": "(?\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))(#)\\s*pragma\\s+mark)\\s+(.*)", "captures": { @@ -210,7 +454,7 @@ "name": "entity.other.attribute-name.pragma.preprocessor.cpp" }, { - "include": "#number_literal" + "include": "#preprocessor_number_literal" }, { "include": "#line_continuation_character" @@ -417,7 +661,7 @@ "include": "#string_context_c" }, { - "include": "#number_literal" + "include": "#preprocessor_number_literal" }, { "include": "#line_continuation_character" @@ -425,7 +669,7 @@ ] }, "diagnostic": { - "name": "meta.preprocessor.diagnostic.cpp", + "name": "meta.preprocessor.diagnostic.$reference(directive).cpp", "begin": "((?:^)((?:(?:(?>\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))(#)\\s*((?:error|warning)))\\b\\s*", "beginCaptures": { "1": { @@ -667,13 +911,13 @@ "begin": "\\G\\s*(\\()", "beginCaptures": { "1": { - "name": "punctuation.definition.parameters.begin.cpp" + "name": "punctuation.definition.parameters.begin.preprocessor.cpp" } }, "end": "(\\))", "endCaptures": { "1": { - "name": "punctuation.definition.parameters.end.cpp" + "name": "punctuation.definition.parameters.end.preprocessor.cpp" } }, "patterns": [ @@ -745,7 +989,7 @@ }, "patterns": [ { - "name": "meta.conditional.preprocessor.cpp", + "name": "meta.preprocessor.conditional.cpp", "begin": "\\G(?<=ifndef|ifdef|if)", "end": "(? { } } - const dataPaths: string[] = params.initializationOptions.dataPaths; + const dataPaths: string[] = params.initializationOptions.dataPaths || []; const customDataProviders = getDataProviders(dataPaths); function getClientCapability(name: string, def: T) { diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 1f3a45c084421..b440901a68cc5 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -781,10 +781,10 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^4.0.3-next.3: - version "4.0.3-next.3" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.3.tgz#eb7f642f2785d388d74a1a98fd14f7736a11e316" - integrity sha512-6j/y9ccecrq7/APLPEijx+uWHsEdTFH5ZQHG4ZMKjZx6euny27B1wvLCjpxKnZCWcHgmi7cMDLWpUdElvHjjPQ== +vscode-css-languageservice@^4.0.3-next.4: + version "4.0.3-next.4" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.4.tgz#3f0ccf116567650597b5cbfd8ce09846a64dec13" + integrity sha512-9eaKw6ez+l407/uzIho51ElMqGSJcOV+M5B/HmAtdBPSc/chkAfx3r7zXOqrAlLKsBzZNVpnsA3C3YDjyZbrdg== dependencies: vscode-languageserver-types "^3.15.0-next.2" vscode-nls "^4.1.1" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index 9c07a2568aa94..50c97fb26f41b 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -19,7 +19,7 @@ "watch": "gulp watch-extension:extension-editing" }, "dependencies": { - "jsonc-parser": "^2.0.2", + "jsonc-parser": "^2.1.1", "markdown-it": "^8.3.1", "parse5": "^3.0.2", "vscode-nls": "^4.0.0" diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 971bf580ba7e8..fb98728f237d7 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -29,10 +29,10 @@ entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" integrity sha1-blwtClYhtdra7O+AuQ7ftc13cvA= -jsonc-parser@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" - integrity sha512-TSU435K5tEKh3g7bam1AFf+uZrISheoDsLlpmAo6wWZYqjsnd09lHYK1Qo+moK4Ikifev1Gdpa69g4NELKnCrQ== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== linkify-it@^2.0.0: version "2.0.3" diff --git a/extensions/git/package.json b/extensions/git/package.json index ffa34ca6efb2b..66b8731c6d9b0 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1354,6 +1354,15 @@ "scope": "resource", "default": false, "description": "%config.supportCancellation%" + }, + "git.branchSortOrder": { + "type": "string", + "enum": [ + "committerdate", + "alphabetically" + ], + "default": "committerdate", + "description": "%config.branchSortOrder%" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 367544c6cc3ed..7d90b4b15f2bd 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -127,6 +127,7 @@ "config.confirmForcePush": "Controls whether to ask for confirmation before force-pushing.", "config.openDiffOnClick": "Controls whether the diff editor should be opened when clicking a change. Otherwise the regular editor will be opened.", "config.supportCancellation": "Controls whether a notification comes up when running the Sync action, which allows the user to cancel the operation.", + "config.branchSortOrder": "Controls the sort order for branches.", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", diff --git a/extensions/git/resources/icons/dark/check.svg b/extensions/git/resources/icons/dark/check.svg index 865cc83c347af..2d16f39007839 100644 --- a/extensions/git/resources/icons/dark/check.svg +++ b/extensions/git/resources/icons/dark/check.svg @@ -1,3 +1,3 @@ - + diff --git a/extensions/git/resources/icons/light/check.svg b/extensions/git/resources/icons/light/check.svg index e1a546660ed15..a9f8aa131b59f 100644 --- a/extensions/git/resources/icons/light/check.svg +++ b/extensions/git/resources/icons/light/check.svg @@ -1,3 +1,3 @@ - + diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index c03c1239722df..412cc5df92f0e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1624,8 +1624,14 @@ export class Repository { .map(([ref]) => ({ name: ref, type: RefType.Head } as Branch)); } - async getRefs(): Promise { - const result = await this.run(['for-each-ref', '--format', '%(refname) %(objectname)', '--sort', '-committerdate']); + async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate' }): Promise { + const args = ['for-each-ref', '--format', '%(refname) %(objectname)']; + + if (opts && opts.sort && opts.sort !== 'alphabetically') { + args.push('--sort', opts.sort); + } + + const result = await this.run(args); const fn = (line: string): Ref | null => { let match: RegExpExecArray | null; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 40b31dff92312..bf37d036d5ca1 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -701,6 +701,9 @@ export class Repository implements Disposable { onConfigListener(updateIndexGroupVisibility, this, this.disposables); updateIndexGroupVisibility(); + const onConfigListenerForBranchSortOrder = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchSortOrder', root)); + onConfigListenerForBranchSortOrder(this.updateModelState, this, this.disposables); + this.mergeGroup.hideWhenEmpty = true; this.disposables.push(this.mergeGroup); @@ -1405,7 +1408,6 @@ export class Repository implements Disposable { const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLimitWarning') === true; const useIcons = !config.get('decorations.enabled', true); - this.isRepositoryHuge = didHitLimit; if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) { @@ -1455,7 +1457,8 @@ export class Repository implements Disposable { // noop } - const [refs, remotes, submodules, rebaseCommit] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes(), this.repository.getSubmodules(), this.getRebaseCommit()]); + const sort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder') || 'alphabetically'; + const [refs, remotes, submodules, rebaseCommit] = await Promise.all([this.repository.getRefs({ sort }), this.repository.getRemotes(), this.repository.getSubmodules(), this.getRebaseCommit()]); this._HEAD = HEAD; this._refs = refs; diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 9b26165edbdfc..3fa4cd14d3cd7 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,8 +9,8 @@ }, "main": "./out/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.3", - "vscode-html-languageservice": "^3.0.4-next.0", + "vscode-css-languageservice": "^4.0.3-next.4", + "vscode-html-languageservice": "^3.0.4-next.1", "vscode-languageserver": "^5.3.0-next.8", "vscode-languageserver-types": "3.15.0-next.2", "vscode-nls": "^4.1.1", diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index f8687c97f6352..94a2693189a73 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -229,19 +229,19 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^4.0.3-next.3: - version "4.0.3-next.3" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.3.tgz#eb7f642f2785d388d74a1a98fd14f7736a11e316" - integrity sha512-6j/y9ccecrq7/APLPEijx+uWHsEdTFH5ZQHG4ZMKjZx6euny27B1wvLCjpxKnZCWcHgmi7cMDLWpUdElvHjjPQ== +vscode-css-languageservice@^4.0.3-next.4: + version "4.0.3-next.4" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.4.tgz#3f0ccf116567650597b5cbfd8ce09846a64dec13" + integrity sha512-9eaKw6ez+l407/uzIho51ElMqGSJcOV+M5B/HmAtdBPSc/chkAfx3r7zXOqrAlLKsBzZNVpnsA3C3YDjyZbrdg== dependencies: vscode-languageserver-types "^3.15.0-next.2" vscode-nls "^4.1.1" vscode-uri "^2.0.3" -vscode-html-languageservice@^3.0.4-next.0: - version "3.0.4-next.0" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.0.tgz#d4f5a103b94753a19b374158212fe734dbe670e8" - integrity sha512-5Z5ITtokWt/zuPKemKEXfC+4XHoQryBAZVAcTwpAel2qqueUwGqjd5ZrVy/2x5GZAdZAipl0BvsTTMkOBS1BFQ== +vscode-html-languageservice@^3.0.4-next.1: + version "3.0.4-next.1" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.1.tgz#fcab383db185cb03cfb10d9039d865a7b243c6e8" + integrity sha512-WgqGH7nDhijveDqrSp9zhcOKUGgOBn4FWO5HzfZV4LnfzaFG7hLMbtbplblJVpqLR6lhTWM8B6E4BlFg/szhWw== dependencies: vscode-languageserver-types "^3.15.0-next.2" vscode-nls "^4.1.1" diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index 6f1ca02d58041..52441738fef19 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -145,11 +145,14 @@ export function activate(context: ExtensionContext) { } }); + const schemaDocuments: { [uri: string]: boolean } = {}; + // handle content request client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { let uri = Uri.parse(uriPath); if (uri.scheme !== 'http' && uri.scheme !== 'https') { return workspace.openTextDocument(uri).then(doc => { + schemaDocuments[uri.toString()] = true; return doc.getText(); }, error => { return Promise.reject(error); @@ -164,10 +167,12 @@ export function activate(context: ExtensionContext) { } }); - let handleContentChange = (uri: Uri) => { - if (uri.scheme === 'vscode' && uri.authority === 'schemas') { - client.sendNotification(SchemaContentChangeNotification.type, uri.toString()); + let handleContentChange = (uriString: string) => { + if (schemaDocuments[uriString]) { + client.sendNotification(SchemaContentChangeNotification.type, uriString); + return true; } + return false; }; let handleActiveEditorChange = (activeEditor?: TextEditor) => { @@ -184,10 +189,13 @@ export function activate(context: ExtensionContext) { } }; - toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri))); + toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); toDispose.push(workspace.onDidCloseTextDocument(d => { - handleContentChange(d.uri); - fileSchemaErrors.delete(d.uri.toString()); + const uriString = d.uri.toString(); + if (handleContentChange(uriString)) { + delete schemaDocuments[uriString]; + } + fileSchemaErrors.delete(uriString); })); toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index bf5e3c8b09a79..bf76da32dba26 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -12,7 +12,7 @@ }, "main": "./out/jsonServerMain", "dependencies": { - "jsonc-parser": "^2.1.0", + "jsonc-parser": "^2.1.1", "request-light": "^0.2.4", "vscode-json-languageservice": "^3.3.1", "vscode-languageserver": "^5.3.0-next.8", diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 3bfcb75ac7da7..3c176e126036b 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -59,6 +59,11 @@ jsonc-parser@^2.1.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.0.tgz#eb0d0c7a3c33048524ce3574c57c7278fb2f1bf3" integrity sha512-n9GrT8rrr2fhvBbANa1g+xFmgGK5X91KFeDwlKQ3+SJfmH5+tKv/M/kahx/TXOMflfWHKGKqKyfHQaLKTNzJ6w== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index f63c46a878d6d..3fe94d8cf7eb5 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/a595d8ba2ae9ce8864435d33db2afa0fe68b1487", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/eb3898715b50d7911377a650e383a768a3a21f25", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -716,7 +716,7 @@ { "begin": "(^|\\G)(\\s*)(.*)", "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", - "contentName": "meta.embedded.block.cpp source.cpp", + "contentName": "meta.embedded.block.cpp", "patterns": [ { "include": "source.cpp" @@ -1855,12 +1855,12 @@ "name": "markup.fenced_code.block.markdown" }, "heading": { - "match": "(?:^|\\G)[ ]{0,3}((#{1,6})\\s*(?=[\\S[^#]]).*?\\s*(#{1,6})?)$\\n?", + "match": "(?:^|\\G)[ ]{0,3}((#{1,6})\\s+(?=[\\S[^#]]).*?\\s*(#{1,6})?)$\\n?", "captures": { "1": { "patterns": [ { - "match": "(#{6})\\s*(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", + "match": "(#{6})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", "name": "heading.6.markdown", "captures": { "1": { @@ -1875,7 +1875,7 @@ } }, { - "match": "(#{5})\\s*(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", + "match": "(#{5})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", "name": "heading.5.markdown", "captures": { "1": { @@ -1890,7 +1890,7 @@ } }, { - "match": "(#{4})\\s*(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", + "match": "(#{4})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", "name": "heading.4.markdown", "captures": { "1": { @@ -1905,7 +1905,7 @@ } }, { - "match": "(#{3})\\s*(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", + "match": "(#{3})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", "name": "heading.3.markdown", "captures": { "1": { @@ -1920,7 +1920,7 @@ } }, { - "match": "(#{2})\\s*(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", + "match": "(#{2})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", "name": "heading.2.markdown", "captures": { "1": { @@ -1935,7 +1935,7 @@ } }, { - "match": "(#{1})\\s*(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", + "match": "(#{1})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?", "name": "heading.1.markdown", "captures": { "1": { @@ -1987,25 +1987,46 @@ "name": "comment.block.html" }, { - "begin": "(^|\\G)\\s*(?=<(script|style|pre)(\\s|$|>)(?!.*?))", - "end": "(?=.*)", + "begin": "(?i)(^|\\G)\\s*(?=<(script|style|pre)(\\s|$|>)(?!.*?))", + "end": "(?i)(.*)(())", + "endCaptures": { + "1": { + "patterns": [ + { + "include": "text.html.derivative" + } + ] + }, + "2": { + "name": "meta.tag.structure.$4.end.html" + }, + "3": { + "name": "punctuation.definition.tag.begin.html" + }, + "4": { + "name": "entity.name.tag.html" + }, + "5": { + "name": "punctuation.definition.tag.end.html" + } + }, "patterns": [ { "begin": "(\\s*|$)", "patterns": [ { - "include": "text.html.basic" + "include": "text.html.derivative" } ], - "while": "^(?!.*)" + "while": "(?i)^(?!.*)" } ] }, { - "begin": "(^|\\G)\\s*(?=))", + "begin": "(?i)(^|\\G)\\s*(?=))", "patterns": [ { - "include": "text.html.basic" + "include": "text.html.derivative" } ], "while": "^(?!\\s*$)" @@ -2014,7 +2035,7 @@ "begin": "(^|\\G)\\s*(?=(<[a-zA-Z0-9\\-](/?>|\\s.*?>)|)\\s*$)", "patterns": [ { - "include": "text.html.basic" + "include": "text.html.derivative" } ], "while": "^(?!\\s*$)" @@ -2074,7 +2095,7 @@ "include": "#inline" }, { - "include": "text.html.basic" + "include": "text.html.derivative" }, { "include": "#heading-setext" @@ -2131,7 +2152,7 @@ "include": "#inline" }, { - "include": "text.html.basic" + "include": "text.html.derivative" }, { "include": "#heading-setext" @@ -2225,7 +2246,7 @@ "end": "(?<=>)", "patterns": [ { - "include": "text.html.basic" + "include": "text.html.derivative" } ] }, @@ -2275,7 +2296,7 @@ }, "bracket": { "comment": "Markdown will convert this for us. We match it so that the HTML grammar will not mark it up as invalid.", - "match": "<(?![a-z/?\\$!])", + "match": "<(?![a-zA-Z/?\\$!])", "name": "meta.other.valid-bracket.markdown" }, "escape": { @@ -2370,7 +2391,7 @@ "end": "(?<=>)", "patterns": [ { - "include": "text.html.basic" + "include": "text.html.derivative" } ] }, @@ -2566,4 +2587,4 @@ "name": "markup.inline.raw.string.markdown" } } -} +} \ No newline at end of file diff --git a/extensions/markdown-basics/test/colorize-results/test-33886_md.json b/extensions/markdown-basics/test/colorize-results/test-33886_md.json index 179172a57382c..25799e89a3630 100644 --- a/extensions/markdown-basics/test/colorize-results/test-33886_md.json +++ b/extensions/markdown-basics/test/colorize-results/test-33886_md.json @@ -111,7 +111,7 @@ }, { "c": "", - "t": "text.html.markdown meta.paragraph.markdown meta.tag.inline.code.end.html punctuation.definition.tag.end.html", + "t": "text.html.markdown meta.tag.inline.code.end.html punctuation.definition.tag.end.html", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -144,7 +144,7 @@ }, { "c": "", - "t": "text.html.markdown meta.paragraph.markdown meta.tag.structure.pre.end.html punctuation.definition.tag.end.html", + "t": "text.html.markdown meta.tag.structure.pre.end.html punctuation.definition.tag.end.html", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -254,7 +254,7 @@ }, { "c": "a", - "t": "text.html.markdown meta.paragraph.markdown", + "t": "text.html.markdown", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -265,7 +265,7 @@ }, { "c": "", - "t": "text.html.markdown meta.paragraph.markdown meta.tag.structure.pre.end.html punctuation.definition.tag.end.html", + "t": "text.html.markdown meta.tag.structure.pre.end.html punctuation.definition.tag.end.html", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", diff --git a/extensions/markdown-language-features/media/index.js b/extensions/markdown-language-features/media/index.js index 00afdf196af75..9a0fd91b45a61 100644 --- a/extensions/markdown-language-features/media/index.js +++ b/extensions/markdown-language-features/media/index.js @@ -659,10 +659,20 @@ window.onload = () => { events_1.onceDocumentLoaded(() => { if (settings.scrollPreviewWithEditor) { setTimeout(() => { - const initialLine = +settings.line; - if (!isNaN(initialLine)) { - scrollDisabled = true; - scroll_sync_1.scrollToRevealSourceLine(initialLine); + // Try to scroll to fragment if available + if (state.fragment) { + const element = scroll_sync_1.getLineElementForFragment(state.fragment); + if (element) { + scrollDisabled = true; + scroll_sync_1.scrollToRevealSourceLine(element.line); + } + } + else { + const initialLine = +settings.line; + if (!isNaN(initialLine)) { + scrollDisabled = true; + scroll_sync_1.scrollToRevealSourceLine(initialLine); + } } }, 0); } @@ -940,6 +950,15 @@ function getEditorLineNumberForPageOffset(offset) { return null; } exports.getEditorLineNumberForPageOffset = getEditorLineNumberForPageOffset; +/** + * Try to find the html element by using a fragment id + */ +function getLineElementForFragment(fragment) { + return getCodeLineElements().find((element) => { + return element.element.id === fragment; + }); +} +exports.getLineElementForFragment = getLineElementForFragment; /***/ }), @@ -986,4 +1005,4 @@ exports.getSettings = getSettings; /***/ }) /******/ }); -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vbm9kZV9tb2R1bGVzL2xvZGFzaC50aHJvdHRsZS9pbmRleC5qcyIsIndlYnBhY2s6Ly8vKHdlYnBhY2spL2J1aWxkaW4vZ2xvYmFsLmpzIiwid2VicGFjazovLy8uL3ByZXZpZXctc3JjL2FjdGl2ZUxpbmVNYXJrZXIudHMiLCJ3ZWJwYWNrOi8vLy4vcHJldmlldy1zcmMvZXZlbnRzLnRzIiwid2VicGFjazovLy8uL3ByZXZpZXctc3JjL2luZGV4LnRzIiwid2VicGFjazovLy8uL3ByZXZpZXctc3JjL21lc3NhZ2luZy50cyIsIndlYnBhY2s6Ly8vLi9wcmV2aWV3LXNyYy9zY3JvbGwtc3luYy50cyIsIndlYnBhY2s6Ly8vLi9wcmV2aWV3LXNyYy9zZXR0aW5ncy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7OztBQUdBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQUs7QUFDTDtBQUNBOztBQUVBO0FBQ0E7QUFDQSx5REFBaUQsY0FBYztBQUMvRDs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxtQ0FBMkIsMEJBQTBCLEVBQUU7QUFDdkQseUNBQWlDLGVBQWU7QUFDaEQ7QUFDQTtBQUNBOztBQUVBO0FBQ0EsOERBQXNELCtEQUErRDs7QUFFckg7QUFDQTs7O0FBR0E7QUFDQTs7Ozs7Ozs7Ozs7O0FDbkVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsYUFBYSxPQUFPO0FBQ3BCO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLFNBQVM7QUFDcEIsV0FBVyxPQUFPO0FBQ2xCLFdBQVcsT0FBTyxZQUFZO0FBQzlCLFdBQVcsUUFBUTtBQUNuQjtBQUNBLFdBQVcsT0FBTztBQUNsQjtBQUNBLFdBQVcsUUFBUTtBQUNuQjtBQUNBLGFBQWEsU0FBUztBQUN0QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBLDhDQUE4QyxrQkFBa0I7QUFDaEU7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLFNBQVM7QUFDcEIsV0FBVyxPQUFPO0FBQ2xCLFdBQVcsT0FBTyxZQUFZO0FBQzlCLFdBQVcsUUFBUTtBQUNuQjtBQUNBLFdBQVcsUUFBUTtBQUNuQjtBQUNBLGFBQWEsU0FBUztBQUN0QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxtREFBbUQsb0JBQW9CO0FBQ3ZFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSDs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLEVBQUU7QUFDYixhQUFhLFFBQVE7QUFDckI7QUFDQTtBQUNBLGdCQUFnQjtBQUNoQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFdBQVcsRUFBRTtBQUNiLGFBQWEsUUFBUTtBQUNyQjtBQUNBO0FBQ0Esb0JBQW9CO0FBQ3BCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLEVBQUU7QUFDYixhQUFhLFFBQVE7QUFDckI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsV0FBVyxFQUFFO0FBQ2IsYUFBYSxPQUFPO0FBQ3BCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOzs7Ozs7Ozs7Ozs7O0FDdGJBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLENBQUM7O0FBRUQ7QUFDQTtBQUNBO0FBQ0EsQ0FBQztBQUNEO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0EsNENBQTRDOztBQUU1Qzs7Ozs7Ozs7Ozs7Ozs7O0FDbkJBOzs7Z0dBR2dHO0FBQ2hHLCtGQUF5RDtBQUV6RCxNQUFhLGdCQUFnQjtJQUc1Qiw4QkFBOEIsQ0FBQyxJQUFZO1FBQzFDLE1BQU0sRUFBRSxRQUFRLEVBQUUsR0FBRyxzQ0FBd0IsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNwRCxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsSUFBSSxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUVELE9BQU8sQ0FBQyxNQUErQjtRQUN0QyxJQUFJLENBQUMsb0JBQW9CLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3pDLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUNoQyxJQUFJLENBQUMsUUFBUSxHQUFHLE1BQU0sQ0FBQztJQUN4QixDQUFDO0lBRUQsb0JBQW9CLENBQUMsT0FBZ0M7UUFDcEQsSUFBSSxDQUFDLE9BQU8sRUFBRTtZQUNiLE9BQU87U0FDUDtRQUNELE9BQU8sQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsdUJBQXVCLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDNUUsQ0FBQztJQUVELGtCQUFrQixDQUFDLE9BQWdDO1FBQ2xELElBQUksQ0FBQyxPQUFPLEVBQUU7WUFDYixPQUFPO1NBQ1A7UUFDRCxPQUFPLENBQUMsU0FBUyxJQUFJLG1CQUFtQixDQUFDO0lBQzFDLENBQUM7Q0FDRDtBQTNCRCw0Q0EyQkM7Ozs7Ozs7Ozs7Ozs7O0FDakNEOzs7Z0dBR2dHOztBQUVoRyxTQUFnQixrQkFBa0IsQ0FBQyxDQUFhO0lBQy9DLElBQUksUUFBUSxDQUFDLFVBQVUsS0FBSyxTQUFTLElBQUksUUFBUSxDQUFDLFVBQW9CLEtBQUssZUFBZSxFQUFFO1FBQzNGLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxrQkFBa0IsRUFBRSxDQUFDLENBQUMsQ0FBQztLQUNqRDtTQUFNO1FBQ04sQ0FBQyxFQUFFLENBQUM7S0FDSjtBQUNGLENBQUM7QUFORCxnREFNQzs7Ozs7Ozs7Ozs7Ozs7QUNYRDs7O2dHQUdnRzs7QUFFaEcsOEdBQXNEO0FBQ3RELGdGQUE4QztBQUM5Qyx5RkFBb0Q7QUFDcEQsK0ZBQTJGO0FBQzNGLHNGQUFrRDtBQUNsRCx1R0FBNkM7QUFJN0MsSUFBSSxjQUFjLEdBQUcsSUFBSSxDQUFDO0FBQzFCLE1BQU0sTUFBTSxHQUFHLElBQUksbUNBQWdCLEVBQUUsQ0FBQztBQUN0QyxNQUFNLFFBQVEsR0FBRyxzQkFBVyxFQUFFLENBQUM7QUFFL0IsTUFBTSxNQUFNLEdBQUcsZ0JBQWdCLEVBQUUsQ0FBQztBQUVsQyxvQkFBb0I7QUFDcEIsSUFBSSxLQUFLLEdBQUcsa0JBQU8sQ0FBbUIsWUFBWSxDQUFDLENBQUM7QUFDcEQsTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUV2QixNQUFNLFNBQVMsR0FBRyxpQ0FBcUIsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUVoRCxNQUFNLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUN2QyxNQUFNLENBQUMsbUJBQW1CLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBRWhELE1BQU0sQ0FBQyxNQUFNLEdBQUcsR0FBRyxFQUFFO0lBQ3BCLGdCQUFnQixFQUFFLENBQUM7QUFDcEIsQ0FBQyxDQUFDO0FBRUYsMkJBQWtCLENBQUMsR0FBRyxFQUFFO0lBQ3ZCLElBQUksUUFBUSxDQUFDLHVCQUF1QixFQUFFO1FBQ3JDLFVBQVUsQ0FBQyxHQUFHLEVBQUU7WUFDZixNQUFNLFdBQVcsR0FBRyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUM7WUFDbkMsSUFBSSxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUMsRUFBRTtnQkFDeEIsY0FBYyxHQUFHLElBQUksQ0FBQztnQkFDdEIsc0NBQXdCLENBQUMsV0FBVyxDQUFDLENBQUM7YUFDdEM7UUFDRixDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7S0FDTjtBQUNGLENBQUMsQ0FBQyxDQUFDO0FBRUgsTUFBTSxZQUFZLEdBQUcsQ0FBQyxHQUFHLEVBQUU7SUFDMUIsTUFBTSxRQUFRLEdBQUcsUUFBUSxDQUFDLENBQUMsSUFBWSxFQUFFLEVBQUU7UUFDMUMsY0FBYyxHQUFHLElBQUksQ0FBQztRQUN0QixzQ0FBd0IsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNoQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFUCxPQUFPLENBQUMsSUFBWSxFQUFFLFFBQWEsRUFBRSxFQUFFO1FBQ3RDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUU7WUFDakIsUUFBUSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7WUFDckIsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO1NBQ2Y7SUFDRixDQUFDLENBQUM7QUFDSCxDQUFDLENBQUMsRUFBRSxDQUFDO0FBRUwsSUFBSSxnQkFBZ0IsR0FBRyxRQUFRLENBQUMsR0FBRyxFQUFFO0lBQ3BDLE1BQU0sU0FBUyxHQUFvRCxFQUFFLENBQUM7SUFDdEUsSUFBSSxNQUFNLEdBQUcsUUFBUSxDQUFDLG9CQUFvQixDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ2xELElBQUksTUFBTSxFQUFFO1FBQ1gsSUFBSSxDQUFDLENBQUM7UUFDTixLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxFQUFFLEVBQUU7WUFDbkMsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBRXRCLElBQUksR0FBRyxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLEVBQUU7Z0JBQ3RDLEdBQUcsQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDO2FBQ2hDO1lBRUQsU0FBUyxDQUFDLElBQUksQ0FBQztnQkFDZCxFQUFFLEVBQUUsR0FBRyxDQUFDLEVBQUU7Z0JBQ1YsTUFBTSxFQUFFLEdBQUcsQ0FBQyxNQUFNO2dCQUNsQixLQUFLLEVBQUUsR0FBRyxDQUFDLEtBQUs7YUFDaEIsQ0FBQyxDQUFDO1NBQ0g7UUFFRCxTQUFTLENBQUMsV0FBVyxDQUFDLGlCQUFpQixFQUFFLFNBQVMsQ0FBQyxDQUFDO0tBQ3BEO0FBQ0YsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBRVAsTUFBTSxDQUFDLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxHQUFHLEVBQUU7SUFDdEMsY0FBYyxHQUFHLElBQUksQ0FBQztJQUN0QixnQkFBZ0IsRUFBRSxDQUFDO0FBQ3BCLENBQUMsRUFBRSxJQUFJLENBQUMsQ0FBQztBQUVULE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsS0FBSyxDQUFDLEVBQUU7SUFDMUMsSUFBSSxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sS0FBSyxRQUFRLENBQUMsTUFBTSxFQUFFO1FBQzFDLE9BQU87S0FDUDtJQUVELFFBQVEsS0FBSyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUU7UUFDeEIsS0FBSyxnQ0FBZ0M7WUFDcEMsTUFBTSxDQUFDLDhCQUE4QixDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDdkQsTUFBTTtRQUVQLEtBQUssWUFBWTtZQUNoQixZQUFZLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUM7WUFDeEMsTUFBTTtLQUNQO0FBQ0YsQ0FBQyxFQUFFLEtBQUssQ0FBQyxDQUFDO0FBRVYsUUFBUSxDQUFDLGdCQUFnQixDQUFDLFVBQVUsRUFBRSxLQUFLLENBQUMsRUFBRTtJQUM3QyxJQUFJLENBQUMsUUFBUSxDQUFDLDJCQUEyQixFQUFFO1FBQzFDLE9BQU87S0FDUDtJQUVELHlCQUF5QjtJQUN6QixLQUFLLElBQUksSUFBSSxHQUFHLEtBQUssQ0FBQyxNQUFxQixFQUFFLElBQUksRUFBRSxJQUFJLEdBQUcsSUFBSSxDQUFDLFVBQXlCLEVBQUU7UUFDekYsSUFBSSxJQUFJLENBQUMsT0FBTyxLQUFLLEdBQUcsRUFBRTtZQUN6QixPQUFPO1NBQ1A7S0FDRDtJQUVELE1BQU0sTUFBTSxHQUFHLEtBQUssQ0FBQyxLQUFLLENBQUM7SUFDM0IsTUFBTSxJQUFJLEdBQUcsOENBQWdDLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDdEQsSUFBSSxPQUFPLElBQUksS0FBSyxRQUFRLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUU7UUFDN0MsU0FBUyxDQUFDLFdBQVcsQ0FBQyxVQUFVLEVBQUUsRUFBRSxJQUFJLEVBQUUsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7S0FDOUQ7QUFDRixDQUFDLENBQUMsQ0FBQztBQUVILFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDLEVBQUU7SUFDMUMsSUFBSSxDQUFDLEtBQUssRUFBRTtRQUNYLE9BQU87S0FDUDtJQUVELElBQUksSUFBSSxHQUFRLEtBQUssQ0FBQyxNQUFNLENBQUM7SUFDN0IsT0FBTyxJQUFJLEVBQUU7UUFDWixJQUFJLElBQUksQ0FBQyxPQUFPLElBQUksSUFBSSxDQUFDLE9BQU8sS0FBSyxHQUFHLElBQUksSUFBSSxDQUFDLElBQUksRUFBRTtZQUN0RCxJQUFJLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxDQUFDLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxFQUFFO2dCQUM5QyxNQUFNO2FBQ047WUFDRCxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLGtCQUFrQixDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLG1CQUFtQixDQUFDLEVBQUU7Z0JBQ3RJLE1BQU0sQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsZ0NBQWdDLEVBQUUsRUFBRSxDQUFDLENBQUMsT0FBTyxDQUFDLElBQUksTUFBTSxDQUFDLElBQUksWUFBWSxDQUFDLFFBQVEsQ0FBQyxtQkFBbUIsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFDbEssU0FBUyxDQUFDLFdBQVcsQ0FBQyxXQUFXLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztnQkFDdkQsS0FBSyxDQUFDLGNBQWMsRUFBRSxDQUFDO2dCQUN2QixLQUFLLENBQUMsZUFBZSxFQUFFLENBQUM7Z0JBQ3hCLE1BQU07YUFDTjtZQUNELE1BQU07U0FDTjtRQUNELElBQUksR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDO0tBQ3ZCO0FBQ0YsQ0FBQyxFQUFFLElBQUksQ0FBQyxDQUFDO0FBRVQsSUFBSSxRQUFRLENBQUMsdUJBQXVCLEVBQUU7SUFDckMsTUFBTSxDQUFDLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQy9DLElBQUksY0FBYyxFQUFFO1lBQ25CLGNBQWMsR0FBRyxLQUFLLENBQUM7U0FDdkI7YUFBTTtZQUNOLE1BQU0sSUFBSSxHQUFHLDhDQUFnQyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztZQUM5RCxJQUFJLE9BQU8sSUFBSSxLQUFLLFFBQVEsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRTtnQkFDN0MsU0FBUyxDQUFDLFdBQVcsQ0FBQyxZQUFZLEVBQUUsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO2dCQUM5QyxLQUFLLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztnQkFDbEIsTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQzthQUN2QjtTQUNEO0lBQ0YsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUM7Q0FDUjtBQUVELFNBQVMsWUFBWSxDQUFDLElBQVk7SUFDakMsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLDBCQUEwQixFQUFFLE1BQU0sQ0FBQyxDQUFDO0FBQ3pELENBQUM7Ozs7Ozs7Ozs7Ozs7O0FDbktEOzs7Z0dBR2dHOztBQUVoRyxzRkFBeUM7QUFTNUIsNkJBQXFCLEdBQUcsQ0FBQyxNQUFXLEVBQUUsRUFBRTtJQUNwRCxPQUFPLElBQUk7UUFDVixXQUFXLENBQUMsSUFBWSxFQUFFLElBQVk7WUFDckMsTUFBTSxDQUFDLFdBQVcsQ0FBQztnQkFDbEIsSUFBSTtnQkFDSixNQUFNLEVBQUUsc0JBQVcsRUFBRSxDQUFDLE1BQU07Z0JBQzVCLElBQUk7YUFDSixDQUFDLENBQUM7UUFDSixDQUFDO0tBQ0QsQ0FBQztBQUNILENBQUMsQ0FBQzs7Ozs7Ozs7Ozs7Ozs7QUN4QkY7OztnR0FHZ0c7O0FBRWhHLHNGQUF5QztBQUd6QyxTQUFTLEtBQUssQ0FBQyxHQUFXLEVBQUUsR0FBVyxFQUFFLEtBQWE7SUFDckQsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBQzVDLENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBQyxJQUFZO0lBQzlCLE9BQU8sS0FBSyxDQUFDLENBQUMsRUFBRSxzQkFBVyxFQUFFLENBQUMsU0FBUyxHQUFHLENBQUMsRUFBRSxJQUFJLENBQUMsQ0FBQztBQUNwRCxDQUFDO0FBUUQsTUFBTSxtQkFBbUIsR0FBRyxDQUFDLEdBQUcsRUFBRTtJQUNqQyxJQUFJLFFBQTJCLENBQUM7SUFDaEMsT0FBTyxHQUFHLEVBQUU7UUFDWCxJQUFJLENBQUMsUUFBUSxFQUFFO1lBQ2QsUUFBUSxHQUFHLENBQUMsRUFBRSxPQUFPLEVBQUUsUUFBUSxDQUFDLElBQUksRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUNqRCxLQUFLLE1BQU0sT0FBTyxJQUFJLFFBQVEsQ0FBQyxzQkFBc0IsQ0FBQyxXQUFXLENBQUMsRUFBRTtnQkFDbkUsTUFBTSxJQUFJLEdBQUcsQ0FBQyxPQUFPLENBQUMsWUFBWSxDQUFDLFdBQVcsQ0FBRSxDQUFDO2dCQUNqRCxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxFQUFFO29CQUNqQixRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLE9BQXNCLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztpQkFDekQ7YUFDRDtTQUNEO1FBQ0QsT0FBTyxRQUFRLENBQUM7SUFDakIsQ0FBQyxDQUFDO0FBQ0gsQ0FBQyxDQUFDLEVBQUUsQ0FBQztBQUVMOzs7OztHQUtHO0FBQ0gsU0FBZ0Isd0JBQXdCLENBQUMsVUFBa0I7SUFDMUQsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUMxQyxNQUFNLEtBQUssR0FBRyxtQkFBbUIsRUFBRSxDQUFDO0lBQ3BDLElBQUksUUFBUSxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxJQUFJLENBQUM7SUFDaEMsS0FBSyxNQUFNLEtBQUssSUFBSSxLQUFLLEVBQUU7UUFDMUIsSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLFVBQVUsRUFBRTtZQUM5QixPQUFPLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsU0FBUyxFQUFFLENBQUM7U0FDNUM7YUFBTSxJQUFJLEtBQUssQ0FBQyxJQUFJLEdBQUcsVUFBVSxFQUFFO1lBQ25DLE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDO1NBQ2pDO1FBQ0QsUUFBUSxHQUFHLEtBQUssQ0FBQztLQUNqQjtJQUNELE9BQU8sRUFBRSxRQUFRLEVBQUUsQ0FBQztBQUNyQixDQUFDO0FBYkQsNERBYUM7QUFFRDs7R0FFRztBQUNILFNBQWdCLDJCQUEyQixDQUFDLE1BQWM7SUFDekQsTUFBTSxLQUFLLEdBQUcsbUJBQW1CLEVBQUUsQ0FBQztJQUNwQyxNQUFNLFFBQVEsR0FBRyxNQUFNLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQztJQUN6QyxJQUFJLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUNaLElBQUksRUFBRSxHQUFHLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDO0lBQzFCLE9BQU8sRUFBRSxHQUFHLENBQUMsR0FBRyxFQUFFLEVBQUU7UUFDbkIsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztRQUN0QyxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxDQUFDLHFCQUFxQixFQUFFLENBQUM7UUFDMUQsSUFBSSxNQUFNLENBQUMsR0FBRyxHQUFHLE1BQU0sQ0FBQyxNQUFNLElBQUksUUFBUSxFQUFFO1lBQzNDLEVBQUUsR0FBRyxHQUFHLENBQUM7U0FDVDthQUNJO1lBQ0osRUFBRSxHQUFHLEdBQUcsQ0FBQztTQUNUO0tBQ0Q7SUFDRCxNQUFNLFNBQVMsR0FBRyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDNUIsTUFBTSxRQUFRLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUFDO0lBQzNELElBQUksRUFBRSxJQUFJLENBQUMsSUFBSSxRQUFRLENBQUMsR0FBRyxHQUFHLFFBQVEsRUFBRTtRQUN2QyxNQUFNLFNBQVMsR0FBRyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDNUIsT0FBTyxFQUFFLFFBQVEsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFDO0tBQ2hEO0lBQ0QsT0FBTyxFQUFFLFFBQVEsRUFBRSxTQUFTLEVBQUUsQ0FBQztBQUNoQyxDQUFDO0FBdEJELGtFQXNCQztBQUVEOztHQUVHO0FBQ0gsU0FBZ0Isd0JBQXdCLENBQUMsSUFBWTtJQUNwRCxJQUFJLENBQUMsc0JBQVcsRUFBRSxDQUFDLHVCQUF1QixFQUFFO1FBQzNDLE9BQU87S0FDUDtJQUVELElBQUksSUFBSSxJQUFJLENBQUMsRUFBRTtRQUNkLE1BQU0sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQztRQUNqQyxPQUFPO0tBQ1A7SUFFRCxNQUFNLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxHQUFHLHdCQUF3QixDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzFELElBQUksQ0FBQyxRQUFRLEVBQUU7UUFDZCxPQUFPO0tBQ1A7SUFDRCxJQUFJLFFBQVEsR0FBRyxDQUFDLENBQUM7SUFDakIsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUFDO0lBQ3RELE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUM7SUFDN0IsSUFBSSxJQUFJLElBQUksSUFBSSxDQUFDLElBQUksS0FBSyxRQUFRLENBQUMsSUFBSSxFQUFFO1FBQ3hDLDhEQUE4RDtRQUM5RCxNQUFNLGVBQWUsR0FBRyxDQUFDLElBQUksR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxHQUFHLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM3RSxNQUFNLGFBQWEsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLHFCQUFxQixFQUFFLENBQUMsR0FBRyxHQUFHLFdBQVcsQ0FBQztRQUM3RSxRQUFRLEdBQUcsV0FBVyxHQUFHLGVBQWUsR0FBRyxhQUFhLENBQUM7S0FDekQ7U0FBTTtRQUNOLE1BQU0saUJBQWlCLEdBQUcsSUFBSSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDbEQsUUFBUSxHQUFHLFdBQVcsR0FBRyxDQUFDLElBQUksQ0FBQyxNQUFNLEdBQUcsaUJBQWlCLENBQUMsQ0FBQztLQUMzRDtJQUNELE1BQU0sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxNQUFNLENBQUMsT0FBTyxHQUFHLFFBQVEsQ0FBQyxDQUFDLENBQUM7QUFDdkUsQ0FBQztBQTNCRCw0REEyQkM7QUFFRCxTQUFnQixnQ0FBZ0MsQ0FBQyxNQUFjO0lBQzlELE1BQU0sRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLEdBQUcsMkJBQTJCLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDL0QsSUFBSSxRQUFRLEVBQUU7UUFDYixNQUFNLGNBQWMsR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLHFCQUFxQixFQUFFLENBQUM7UUFDaEUsTUFBTSxrQkFBa0IsR0FBRyxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsT0FBTyxHQUFHLGNBQWMsQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUMxRSxJQUFJLElBQUksRUFBRTtZQUNULE1BQU0sdUJBQXVCLEdBQUcsa0JBQWtCLEdBQUcsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLHFCQUFxQixFQUFFLENBQUMsR0FBRyxHQUFHLGNBQWMsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUNySCxNQUFNLElBQUksR0FBRyxRQUFRLENBQUMsSUFBSSxHQUFHLHVCQUF1QixHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDbkYsT0FBTyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDdkI7YUFDSTtZQUNKLE1BQU0scUJBQXFCLEdBQUcsa0JBQWtCLEdBQUcsQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLENBQUM7WUFDM0UsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLElBQUksR0FBRyxxQkFBcUIsQ0FBQztZQUNuRCxPQUFPLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUN2QjtLQUNEO0lBQ0QsT0FBTyxJQUFJLENBQUM7QUFDYixDQUFDO0FBakJELDRFQWlCQzs7Ozs7Ozs7Ozs7Ozs7QUN2SUQ7OztnR0FHZ0c7O0FBYWhHLElBQUksY0FBYyxHQUFnQyxTQUFTLENBQUM7QUFFNUQsU0FBZ0IsT0FBTyxDQUFTLEdBQVc7SUFDMUMsTUFBTSxPQUFPLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyw4QkFBOEIsQ0FBQyxDQUFDO0lBQ3hFLElBQUksT0FBTyxFQUFFO1FBQ1osTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUN2QyxJQUFJLElBQUksRUFBRTtZQUNULE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUN4QjtLQUNEO0lBRUQsTUFBTSxJQUFJLEtBQUssQ0FBQywyQkFBMkIsR0FBRyxFQUFFLENBQUMsQ0FBQztBQUNuRCxDQUFDO0FBVkQsMEJBVUM7QUFFRCxTQUFnQixXQUFXO0lBQzFCLElBQUksY0FBYyxFQUFFO1FBQ25CLE9BQU8sY0FBYyxDQUFDO0tBQ3RCO0lBRUQsY0FBYyxHQUFHLE9BQU8sQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUMxQyxJQUFJLGNBQWMsRUFBRTtRQUNuQixPQUFPLGNBQWMsQ0FBQztLQUN0QjtJQUVELE1BQU0sSUFBSSxLQUFLLENBQUMseUJBQXlCLENBQUMsQ0FBQztBQUM1QyxDQUFDO0FBWEQsa0NBV0MiLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VzQ29udGVudCI6WyIgXHQvLyBUaGUgbW9kdWxlIGNhY2hlXG4gXHR2YXIgaW5zdGFsbGVkTW9kdWxlcyA9IHt9O1xuXG4gXHQvLyBUaGUgcmVxdWlyZSBmdW5jdGlvblxuIFx0ZnVuY3Rpb24gX193ZWJwYWNrX3JlcXVpcmVfXyhtb2R1bGVJZCkge1xuXG4gXHRcdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuIFx0XHRpZihpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSkge1xuIFx0XHRcdHJldHVybiBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXS5leHBvcnRzO1xuIFx0XHR9XG4gXHRcdC8vIENyZWF0ZSBhIG5ldyBtb2R1bGUgKGFuZCBwdXQgaXQgaW50byB0aGUgY2FjaGUpXG4gXHRcdHZhciBtb2R1bGUgPSBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSA9IHtcbiBcdFx0XHRpOiBtb2R1bGVJZCxcbiBcdFx0XHRsOiBmYWxzZSxcbiBcdFx0XHRleHBvcnRzOiB7fVxuIFx0XHR9O1xuXG4gXHRcdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuIFx0XHRtb2R1bGVzW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuIFx0XHQvLyBGbGFnIHRoZSBtb2R1bGUgYXMgbG9hZGVkXG4gXHRcdG1vZHVsZS5sID0gdHJ1ZTtcblxuIFx0XHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuIFx0XHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG4gXHR9XG5cblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGVzIG9iamVjdCAoX193ZWJwYWNrX21vZHVsZXNfXylcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubSA9IG1vZHVsZXM7XG5cbiBcdC8vIGV4cG9zZSB0aGUgbW9kdWxlIGNhY2hlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmMgPSBpbnN0YWxsZWRNb2R1bGVzO1xuXG4gXHQvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9uIGZvciBoYXJtb255IGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uZCA9IGZ1bmN0aW9uKGV4cG9ydHMsIG5hbWUsIGdldHRlcikge1xuIFx0XHRpZighX193ZWJwYWNrX3JlcXVpcmVfXy5vKGV4cG9ydHMsIG5hbWUpKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIG5hbWUsIHtcbiBcdFx0XHRcdGNvbmZpZ3VyYWJsZTogZmFsc2UsXG4gXHRcdFx0XHRlbnVtZXJhYmxlOiB0cnVlLFxuIFx0XHRcdFx0Z2V0OiBnZXR0ZXJcbiBcdFx0XHR9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gZ2V0RGVmYXVsdEV4cG9ydCBmdW5jdGlvbiBmb3IgY29tcGF0aWJpbGl0eSB3aXRoIG5vbi1oYXJtb255IG1vZHVsZXNcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubiA9IGZ1bmN0aW9uKG1vZHVsZSkge1xuIFx0XHR2YXIgZ2V0dGVyID0gbW9kdWxlICYmIG1vZHVsZS5fX2VzTW9kdWxlID9cbiBcdFx0XHRmdW5jdGlvbiBnZXREZWZhdWx0KCkgeyByZXR1cm4gbW9kdWxlWydkZWZhdWx0J107IH0gOlxuIFx0XHRcdGZ1bmN0aW9uIGdldE1vZHVsZUV4cG9ydHMoKSB7IHJldHVybiBtb2R1bGU7IH07XG4gXHRcdF9fd2VicGFja19yZXF1aXJlX18uZChnZXR0ZXIsICdhJywgZ2V0dGVyKTtcbiBcdFx0cmV0dXJuIGdldHRlcjtcbiBcdH07XG5cbiBcdC8vIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbFxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5vID0gZnVuY3Rpb24ob2JqZWN0LCBwcm9wZXJ0eSkgeyByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKG9iamVjdCwgcHJvcGVydHkpOyB9O1xuXG4gXHQvLyBfX3dlYnBhY2tfcHVibGljX3BhdGhfX1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5wID0gXCJcIjtcblxuXG4gXHQvLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbiBcdHJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKF9fd2VicGFja19yZXF1aXJlX18ucyA9IFwiLi9wcmV2aWV3LXNyYy9pbmRleC50c1wiKTtcbiIsIi8qKlxuICogbG9kYXNoIChDdXN0b20gQnVpbGQpIDxodHRwczovL2xvZGFzaC5jb20vPlxuICogQnVpbGQ6IGBsb2Rhc2ggbW9kdWxhcml6ZSBleHBvcnRzPVwibnBtXCIgLW8gLi9gXG4gKiBDb3B5cmlnaHQgalF1ZXJ5IEZvdW5kYXRpb24gYW5kIG90aGVyIGNvbnRyaWJ1dG9ycyA8aHR0cHM6Ly9qcXVlcnkub3JnLz5cbiAqIFJlbGVhc2VkIHVuZGVyIE1JVCBsaWNlbnNlIDxodHRwczovL2xvZGFzaC5jb20vbGljZW5zZT5cbiAqIEJhc2VkIG9uIFVuZGVyc2NvcmUuanMgMS44LjMgPGh0dHA6Ly91bmRlcnNjb3JlanMub3JnL0xJQ0VOU0U+XG4gKiBDb3B5cmlnaHQgSmVyZW15IEFzaGtlbmFzLCBEb2N1bWVudENsb3VkIGFuZCBJbnZlc3RpZ2F0aXZlIFJlcG9ydGVycyAmIEVkaXRvcnNcbiAqL1xuXG4vKiogVXNlZCBhcyB0aGUgYFR5cGVFcnJvcmAgbWVzc2FnZSBmb3IgXCJGdW5jdGlvbnNcIiBtZXRob2RzLiAqL1xudmFyIEZVTkNfRVJST1JfVEVYVCA9ICdFeHBlY3RlZCBhIGZ1bmN0aW9uJztcblxuLyoqIFVzZWQgYXMgcmVmZXJlbmNlcyBmb3IgdmFyaW91cyBgTnVtYmVyYCBjb25zdGFudHMuICovXG52YXIgTkFOID0gMCAvIDA7XG5cbi8qKiBgT2JqZWN0I3RvU3RyaW5nYCByZXN1bHQgcmVmZXJlbmNlcy4gKi9cbnZhciBzeW1ib2xUYWcgPSAnW29iamVjdCBTeW1ib2xdJztcblxuLyoqIFVzZWQgdG8gbWF0Y2ggbGVhZGluZyBhbmQgdHJhaWxpbmcgd2hpdGVzcGFjZS4gKi9cbnZhciByZVRyaW0gPSAvXlxccyt8XFxzKyQvZztcblxuLyoqIFVzZWQgdG8gZGV0ZWN0IGJhZCBzaWduZWQgaGV4YWRlY2ltYWwgc3RyaW5nIHZhbHVlcy4gKi9cbnZhciByZUlzQmFkSGV4ID0gL15bLStdMHhbMC05YS1mXSskL2k7XG5cbi8qKiBVc2VkIHRvIGRldGVjdCBiaW5hcnkgc3RyaW5nIHZhbHVlcy4gKi9cbnZhciByZUlzQmluYXJ5ID0gL14wYlswMV0rJC9pO1xuXG4vKiogVXNlZCB0byBkZXRlY3Qgb2N0YWwgc3RyaW5nIHZhbHVlcy4gKi9cbnZhciByZUlzT2N0YWwgPSAvXjBvWzAtN10rJC9pO1xuXG4vKiogQnVpbHQtaW4gbWV0aG9kIHJlZmVyZW5jZXMgd2l0aG91dCBhIGRlcGVuZGVuY3kgb24gYHJvb3RgLiAqL1xudmFyIGZyZWVQYXJzZUludCA9IHBhcnNlSW50O1xuXG4vKiogRGV0ZWN0IGZyZWUgdmFyaWFibGUgYGdsb2JhbGAgZnJvbSBOb2RlLmpzLiAqL1xudmFyIGZyZWVHbG9iYWwgPSB0eXBlb2YgZ2xvYmFsID09ICdvYmplY3QnICYmIGdsb2JhbCAmJiBnbG9iYWwuT2JqZWN0ID09PSBPYmplY3QgJiYgZ2xvYmFsO1xuXG4vKiogRGV0ZWN0IGZyZWUgdmFyaWFibGUgYHNlbGZgLiAqL1xudmFyIGZyZWVTZWxmID0gdHlwZW9mIHNlbGYgPT0gJ29iamVjdCcgJiYgc2VsZiAmJiBzZWxmLk9iamVjdCA9PT0gT2JqZWN0ICYmIHNlbGY7XG5cbi8qKiBVc2VkIGFzIGEgcmVmZXJlbmNlIHRvIHRoZSBnbG9iYWwgb2JqZWN0LiAqL1xudmFyIHJvb3QgPSBmcmVlR2xvYmFsIHx8IGZyZWVTZWxmIHx8IEZ1bmN0aW9uKCdyZXR1cm4gdGhpcycpKCk7XG5cbi8qKiBVc2VkIGZvciBidWlsdC1pbiBtZXRob2QgcmVmZXJlbmNlcy4gKi9cbnZhciBvYmplY3RQcm90byA9IE9iamVjdC5wcm90b3R5cGU7XG5cbi8qKlxuICogVXNlZCB0byByZXNvbHZlIHRoZVxuICogW2B0b1N0cmluZ1RhZ2BdKGh0dHA6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzcuMC8jc2VjLW9iamVjdC5wcm90b3R5cGUudG9zdHJpbmcpXG4gKiBvZiB2YWx1ZXMuXG4gKi9cbnZhciBvYmplY3RUb1N0cmluZyA9IG9iamVjdFByb3RvLnRvU3RyaW5nO1xuXG4vKiBCdWlsdC1pbiBtZXRob2QgcmVmZXJlbmNlcyBmb3IgdGhvc2Ugd2l0aCB0aGUgc2FtZSBuYW1lIGFzIG90aGVyIGBsb2Rhc2hgIG1ldGhvZHMuICovXG52YXIgbmF0aXZlTWF4ID0gTWF0aC5tYXgsXG4gICAgbmF0aXZlTWluID0gTWF0aC5taW47XG5cbi8qKlxuICogR2V0cyB0aGUgdGltZXN0YW1wIG9mIHRoZSBudW1iZXIgb2YgbWlsbGlzZWNvbmRzIHRoYXQgaGF2ZSBlbGFwc2VkIHNpbmNlXG4gKiB0aGUgVW5peCBlcG9jaCAoMSBKYW51YXJ5IDE5NzAgMDA6MDA6MDAgVVRDKS5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDIuNC4wXG4gKiBAY2F0ZWdvcnkgRGF0ZVxuICogQHJldHVybnMge251bWJlcn0gUmV0dXJucyB0aGUgdGltZXN0YW1wLlxuICogQGV4YW1wbGVcbiAqXG4gKiBfLmRlZmVyKGZ1bmN0aW9uKHN0YW1wKSB7XG4gKiAgIGNvbnNvbGUubG9nKF8ubm93KCkgLSBzdGFtcCk7XG4gKiB9LCBfLm5vdygpKTtcbiAqIC8vID0+IExvZ3MgdGhlIG51bWJlciBvZiBtaWxsaXNlY29uZHMgaXQgdG9vayBmb3IgdGhlIGRlZmVycmVkIGludm9jYXRpb24uXG4gKi9cbnZhciBub3cgPSBmdW5jdGlvbigpIHtcbiAgcmV0dXJuIHJvb3QuRGF0ZS5ub3coKTtcbn07XG5cbi8qKlxuICogQ3JlYXRlcyBhIGRlYm91bmNlZCBmdW5jdGlvbiB0aGF0IGRlbGF5cyBpbnZva2luZyBgZnVuY2AgdW50aWwgYWZ0ZXIgYHdhaXRgXG4gKiBtaWxsaXNlY29uZHMgaGF2ZSBlbGFwc2VkIHNpbmNlIHRoZSBsYXN0IHRpbWUgdGhlIGRlYm91bmNlZCBmdW5jdGlvbiB3YXNcbiAqIGludm9rZWQuIFRoZSBkZWJvdW5jZWQgZnVuY3Rpb24gY29tZXMgd2l0aCBhIGBjYW5jZWxgIG1ldGhvZCB0byBjYW5jZWxcbiAqIGRlbGF5ZWQgYGZ1bmNgIGludm9jYXRpb25zIGFuZCBhIGBmbHVzaGAgbWV0aG9kIHRvIGltbWVkaWF0ZWx5IGludm9rZSB0aGVtLlxuICogUHJvdmlkZSBgb3B0aW9uc2AgdG8gaW5kaWNhdGUgd2hldGhlciBgZnVuY2Agc2hvdWxkIGJlIGludm9rZWQgb24gdGhlXG4gKiBsZWFkaW5nIGFuZC9vciB0cmFpbGluZyBlZGdlIG9mIHRoZSBgd2FpdGAgdGltZW91dC4gVGhlIGBmdW5jYCBpcyBpbnZva2VkXG4gKiB3aXRoIHRoZSBsYXN0IGFyZ3VtZW50cyBwcm92aWRlZCB0byB0aGUgZGVib3VuY2VkIGZ1bmN0aW9uLiBTdWJzZXF1ZW50XG4gKiBjYWxscyB0byB0aGUgZGVib3VuY2VkIGZ1bmN0aW9uIHJldHVybiB0aGUgcmVzdWx0IG9mIHRoZSBsYXN0IGBmdW5jYFxuICogaW52b2NhdGlvbi5cbiAqXG4gKiAqKk5vdGU6KiogSWYgYGxlYWRpbmdgIGFuZCBgdHJhaWxpbmdgIG9wdGlvbnMgYXJlIGB0cnVlYCwgYGZ1bmNgIGlzXG4gKiBpbnZva2VkIG9uIHRoZSB0cmFpbGluZyBlZGdlIG9mIHRoZSB0aW1lb3V0IG9ubHkgaWYgdGhlIGRlYm91bmNlZCBmdW5jdGlvblxuICogaXMgaW52b2tlZCBtb3JlIHRoYW4gb25jZSBkdXJpbmcgdGhlIGB3YWl0YCB0aW1lb3V0LlxuICpcbiAqIElmIGB3YWl0YCBpcyBgMGAgYW5kIGBsZWFkaW5nYCBpcyBgZmFsc2VgLCBgZnVuY2AgaW52b2NhdGlvbiBpcyBkZWZlcnJlZFxuICogdW50aWwgdG8gdGhlIG5leHQgdGljaywgc2ltaWxhciB0byBgc2V0VGltZW91dGAgd2l0aCBhIHRpbWVvdXQgb2YgYDBgLlxuICpcbiAqIFNlZSBbRGF2aWQgQ29yYmFjaG8ncyBhcnRpY2xlXShodHRwczovL2Nzcy10cmlja3MuY29tL2RlYm91bmNpbmctdGhyb3R0bGluZy1leHBsYWluZWQtZXhhbXBsZXMvKVxuICogZm9yIGRldGFpbHMgb3ZlciB0aGUgZGlmZmVyZW5jZXMgYmV0d2VlbiBgXy5kZWJvdW5jZWAgYW5kIGBfLnRocm90dGxlYC5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDAuMS4wXG4gKiBAY2F0ZWdvcnkgRnVuY3Rpb25cbiAqIEBwYXJhbSB7RnVuY3Rpb259IGZ1bmMgVGhlIGZ1bmN0aW9uIHRvIGRlYm91bmNlLlxuICogQHBhcmFtIHtudW1iZXJ9IFt3YWl0PTBdIFRoZSBudW1iZXIgb2YgbWlsbGlzZWNvbmRzIHRvIGRlbGF5LlxuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zPXt9XSBUaGUgb3B0aW9ucyBvYmplY3QuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLmxlYWRpbmc9ZmFsc2VdXG4gKiAgU3BlY2lmeSBpbnZva2luZyBvbiB0aGUgbGVhZGluZyBlZGdlIG9mIHRoZSB0aW1lb3V0LlxuICogQHBhcmFtIHtudW1iZXJ9IFtvcHRpb25zLm1heFdhaXRdXG4gKiAgVGhlIG1heGltdW0gdGltZSBgZnVuY2AgaXMgYWxsb3dlZCB0byBiZSBkZWxheWVkIGJlZm9yZSBpdCdzIGludm9rZWQuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLnRyYWlsaW5nPXRydWVdXG4gKiAgU3BlY2lmeSBpbnZva2luZyBvbiB0aGUgdHJhaWxpbmcgZWRnZSBvZiB0aGUgdGltZW91dC5cbiAqIEByZXR1cm5zIHtGdW5jdGlvbn0gUmV0dXJucyB0aGUgbmV3IGRlYm91bmNlZCBmdW5jdGlvbi5cbiAqIEBleGFtcGxlXG4gKlxuICogLy8gQXZvaWQgY29zdGx5IGNhbGN1bGF0aW9ucyB3aGlsZSB0aGUgd2luZG93IHNpemUgaXMgaW4gZmx1eC5cbiAqIGpRdWVyeSh3aW5kb3cpLm9uKCdyZXNpemUnLCBfLmRlYm91bmNlKGNhbGN1bGF0ZUxheW91dCwgMTUwKSk7XG4gKlxuICogLy8gSW52b2tlIGBzZW5kTWFpbGAgd2hlbiBjbGlja2VkLCBkZWJvdW5jaW5nIHN1YnNlcXVlbnQgY2FsbHMuXG4gKiBqUXVlcnkoZWxlbWVudCkub24oJ2NsaWNrJywgXy5kZWJvdW5jZShzZW5kTWFpbCwgMzAwLCB7XG4gKiAgICdsZWFkaW5nJzogdHJ1ZSxcbiAqICAgJ3RyYWlsaW5nJzogZmFsc2VcbiAqIH0pKTtcbiAqXG4gKiAvLyBFbnN1cmUgYGJhdGNoTG9nYCBpcyBpbnZva2VkIG9uY2UgYWZ0ZXIgMSBzZWNvbmQgb2YgZGVib3VuY2VkIGNhbGxzLlxuICogdmFyIGRlYm91bmNlZCA9IF8uZGVib3VuY2UoYmF0Y2hMb2csIDI1MCwgeyAnbWF4V2FpdCc6IDEwMDAgfSk7XG4gKiB2YXIgc291cmNlID0gbmV3IEV2ZW50U291cmNlKCcvc3RyZWFtJyk7XG4gKiBqUXVlcnkoc291cmNlKS5vbignbWVzc2FnZScsIGRlYm91bmNlZCk7XG4gKlxuICogLy8gQ2FuY2VsIHRoZSB0cmFpbGluZyBkZWJvdW5jZWQgaW52b2NhdGlvbi5cbiAqIGpRdWVyeSh3aW5kb3cpLm9uKCdwb3BzdGF0ZScsIGRlYm91bmNlZC5jYW5jZWwpO1xuICovXG5mdW5jdGlvbiBkZWJvdW5jZShmdW5jLCB3YWl0LCBvcHRpb25zKSB7XG4gIHZhciBsYXN0QXJncyxcbiAgICAgIGxhc3RUaGlzLFxuICAgICAgbWF4V2FpdCxcbiAgICAgIHJlc3VsdCxcbiAgICAgIHRpbWVySWQsXG4gICAgICBsYXN0Q2FsbFRpbWUsXG4gICAgICBsYXN0SW52b2tlVGltZSA9IDAsXG4gICAgICBsZWFkaW5nID0gZmFsc2UsXG4gICAgICBtYXhpbmcgPSBmYWxzZSxcbiAgICAgIHRyYWlsaW5nID0gdHJ1ZTtcblxuICBpZiAodHlwZW9mIGZ1bmMgIT0gJ2Z1bmN0aW9uJykge1xuICAgIHRocm93IG5ldyBUeXBlRXJyb3IoRlVOQ19FUlJPUl9URVhUKTtcbiAgfVxuICB3YWl0ID0gdG9OdW1iZXIod2FpdCkgfHwgMDtcbiAgaWYgKGlzT2JqZWN0KG9wdGlvbnMpKSB7XG4gICAgbGVhZGluZyA9ICEhb3B0aW9ucy5sZWFkaW5nO1xuICAgIG1heGluZyA9ICdtYXhXYWl0JyBpbiBvcHRpb25zO1xuICAgIG1heFdhaXQgPSBtYXhpbmcgPyBuYXRpdmVNYXgodG9OdW1iZXIob3B0aW9ucy5tYXhXYWl0KSB8fCAwLCB3YWl0KSA6IG1heFdhaXQ7XG4gICAgdHJhaWxpbmcgPSAndHJhaWxpbmcnIGluIG9wdGlvbnMgPyAhIW9wdGlvbnMudHJhaWxpbmcgOiB0cmFpbGluZztcbiAgfVxuXG4gIGZ1bmN0aW9uIGludm9rZUZ1bmModGltZSkge1xuICAgIHZhciBhcmdzID0gbGFzdEFyZ3MsXG4gICAgICAgIHRoaXNBcmcgPSBsYXN0VGhpcztcblxuICAgIGxhc3RBcmdzID0gbGFzdFRoaXMgPSB1bmRlZmluZWQ7XG4gICAgbGFzdEludm9rZVRpbWUgPSB0aW1lO1xuICAgIHJlc3VsdCA9IGZ1bmMuYXBwbHkodGhpc0FyZywgYXJncyk7XG4gICAgcmV0dXJuIHJlc3VsdDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGxlYWRpbmdFZGdlKHRpbWUpIHtcbiAgICAvLyBSZXNldCBhbnkgYG1heFdhaXRgIHRpbWVyLlxuICAgIGxhc3RJbnZva2VUaW1lID0gdGltZTtcbiAgICAvLyBTdGFydCB0aGUgdGltZXIgZm9yIHRoZSB0cmFpbGluZyBlZGdlLlxuICAgIHRpbWVySWQgPSBzZXRUaW1lb3V0KHRpbWVyRXhwaXJlZCwgd2FpdCk7XG4gICAgLy8gSW52b2tlIHRoZSBsZWFkaW5nIGVkZ2UuXG4gICAgcmV0dXJuIGxlYWRpbmcgPyBpbnZva2VGdW5jKHRpbWUpIDogcmVzdWx0O1xuICB9XG5cbiAgZnVuY3Rpb24gcmVtYWluaW5nV2FpdCh0aW1lKSB7XG4gICAgdmFyIHRpbWVTaW5jZUxhc3RDYWxsID0gdGltZSAtIGxhc3RDYWxsVGltZSxcbiAgICAgICAgdGltZVNpbmNlTGFzdEludm9rZSA9IHRpbWUgLSBsYXN0SW52b2tlVGltZSxcbiAgICAgICAgcmVzdWx0ID0gd2FpdCAtIHRpbWVTaW5jZUxhc3RDYWxsO1xuXG4gICAgcmV0dXJuIG1heGluZyA/IG5hdGl2ZU1pbihyZXN1bHQsIG1heFdhaXQgLSB0aW1lU2luY2VMYXN0SW52b2tlKSA6IHJlc3VsdDtcbiAgfVxuXG4gIGZ1bmN0aW9uIHNob3VsZEludm9rZSh0aW1lKSB7XG4gICAgdmFyIHRpbWVTaW5jZUxhc3RDYWxsID0gdGltZSAtIGxhc3RDYWxsVGltZSxcbiAgICAgICAgdGltZVNpbmNlTGFzdEludm9rZSA9IHRpbWUgLSBsYXN0SW52b2tlVGltZTtcblxuICAgIC8vIEVpdGhlciB0aGlzIGlzIHRoZSBmaXJzdCBjYWxsLCBhY3Rpdml0eSBoYXMgc3RvcHBlZCBhbmQgd2UncmUgYXQgdGhlXG4gICAgLy8gdHJhaWxpbmcgZWRnZSwgdGhlIHN5c3RlbSB0aW1lIGhhcyBnb25lIGJhY2t3YXJkcyBhbmQgd2UncmUgdHJlYXRpbmdcbiAgICAvLyBpdCBhcyB0aGUgdHJhaWxpbmcgZWRnZSwgb3Igd2UndmUgaGl0IHRoZSBgbWF4V2FpdGAgbGltaXQuXG4gICAgcmV0dXJuIChsYXN0Q2FsbFRpbWUgPT09IHVuZGVmaW5lZCB8fCAodGltZVNpbmNlTGFzdENhbGwgPj0gd2FpdCkgfHxcbiAgICAgICh0aW1lU2luY2VMYXN0Q2FsbCA8IDApIHx8IChtYXhpbmcgJiYgdGltZVNpbmNlTGFzdEludm9rZSA+PSBtYXhXYWl0KSk7XG4gIH1cblxuICBmdW5jdGlvbiB0aW1lckV4cGlyZWQoKSB7XG4gICAgdmFyIHRpbWUgPSBub3coKTtcbiAgICBpZiAoc2hvdWxkSW52b2tlKHRpbWUpKSB7XG4gICAgICByZXR1cm4gdHJhaWxpbmdFZGdlKHRpbWUpO1xuICAgIH1cbiAgICAvLyBSZXN0YXJ0IHRoZSB0aW1lci5cbiAgICB0aW1lcklkID0gc2V0VGltZW91dCh0aW1lckV4cGlyZWQsIHJlbWFpbmluZ1dhaXQodGltZSkpO1xuICB9XG5cbiAgZnVuY3Rpb24gdHJhaWxpbmdFZGdlKHRpbWUpIHtcbiAgICB0aW1lcklkID0gdW5kZWZpbmVkO1xuXG4gICAgLy8gT25seSBpbnZva2UgaWYgd2UgaGF2ZSBgbGFzdEFyZ3NgIHdoaWNoIG1lYW5zIGBmdW5jYCBoYXMgYmVlblxuICAgIC8vIGRlYm91bmNlZCBhdCBsZWFzdCBvbmNlLlxuICAgIGlmICh0cmFpbGluZyAmJiBsYXN0QXJncykge1xuICAgICAgcmV0dXJuIGludm9rZUZ1bmModGltZSk7XG4gICAgfVxuICAgIGxhc3RBcmdzID0gbGFzdFRoaXMgPSB1bmRlZmluZWQ7XG4gICAgcmV0dXJuIHJlc3VsdDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGNhbmNlbCgpIHtcbiAgICBpZiAodGltZXJJZCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICBjbGVhclRpbWVvdXQodGltZXJJZCk7XG4gICAgfVxuICAgIGxhc3RJbnZva2VUaW1lID0gMDtcbiAgICBsYXN0QXJncyA9IGxhc3RDYWxsVGltZSA9IGxhc3RUaGlzID0gdGltZXJJZCA9IHVuZGVmaW5lZDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGZsdXNoKCkge1xuICAgIHJldHVybiB0aW1lcklkID09PSB1bmRlZmluZWQgPyByZXN1bHQgOiB0cmFpbGluZ0VkZ2Uobm93KCkpO1xuICB9XG5cbiAgZnVuY3Rpb24gZGVib3VuY2VkKCkge1xuICAgIHZhciB0aW1lID0gbm93KCksXG4gICAgICAgIGlzSW52b2tpbmcgPSBzaG91bGRJbnZva2UodGltZSk7XG5cbiAgICBsYXN0QXJncyA9IGFyZ3VtZW50cztcbiAgICBsYXN0VGhpcyA9IHRoaXM7XG4gICAgbGFzdENhbGxUaW1lID0gdGltZTtcblxuICAgIGlmIChpc0ludm9raW5nKSB7XG4gICAgICBpZiAodGltZXJJZCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBsZWFkaW5nRWRnZShsYXN0Q2FsbFRpbWUpO1xuICAgICAgfVxuICAgICAgaWYgKG1heGluZykge1xuICAgICAgICAvLyBIYW5kbGUgaW52b2NhdGlvbnMgaW4gYSB0aWdodCBsb29wLlxuICAgICAgICB0aW1lcklkID0gc2V0VGltZW91dCh0aW1lckV4cGlyZWQsIHdhaXQpO1xuICAgICAgICByZXR1cm4gaW52b2tlRnVuYyhsYXN0Q2FsbFRpbWUpO1xuICAgICAgfVxuICAgIH1cbiAgICBpZiAodGltZXJJZCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICB0aW1lcklkID0gc2V0VGltZW91dCh0aW1lckV4cGlyZWQsIHdhaXQpO1xuICAgIH1cbiAgICByZXR1cm4gcmVzdWx0O1xuICB9XG4gIGRlYm91bmNlZC5jYW5jZWwgPSBjYW5jZWw7XG4gIGRlYm91bmNlZC5mbHVzaCA9IGZsdXNoO1xuICByZXR1cm4gZGVib3VuY2VkO1xufVxuXG4vKipcbiAqIENyZWF0ZXMgYSB0aHJvdHRsZWQgZnVuY3Rpb24gdGhhdCBvbmx5IGludm9rZXMgYGZ1bmNgIGF0IG1vc3Qgb25jZSBwZXJcbiAqIGV2ZXJ5IGB3YWl0YCBtaWxsaXNlY29uZHMuIFRoZSB0aHJvdHRsZWQgZnVuY3Rpb24gY29tZXMgd2l0aCBhIGBjYW5jZWxgXG4gKiBtZXRob2QgdG8gY2FuY2VsIGRlbGF5ZWQgYGZ1bmNgIGludm9jYXRpb25zIGFuZCBhIGBmbHVzaGAgbWV0aG9kIHRvXG4gKiBpbW1lZGlhdGVseSBpbnZva2UgdGhlbS4gUHJvdmlkZSBgb3B0aW9uc2AgdG8gaW5kaWNhdGUgd2hldGhlciBgZnVuY2BcbiAqIHNob3VsZCBiZSBpbnZva2VkIG9uIHRoZSBsZWFkaW5nIGFuZC9vciB0cmFpbGluZyBlZGdlIG9mIHRoZSBgd2FpdGBcbiAqIHRpbWVvdXQuIFRoZSBgZnVuY2AgaXMgaW52b2tlZCB3aXRoIHRoZSBsYXN0IGFyZ3VtZW50cyBwcm92aWRlZCB0byB0aGVcbiAqIHRocm90dGxlZCBmdW5jdGlvbi4gU3Vic2VxdWVudCBjYWxscyB0byB0aGUgdGhyb3R0bGVkIGZ1bmN0aW9uIHJldHVybiB0aGVcbiAqIHJlc3VsdCBvZiB0aGUgbGFzdCBgZnVuY2AgaW52b2NhdGlvbi5cbiAqXG4gKiAqKk5vdGU6KiogSWYgYGxlYWRpbmdgIGFuZCBgdHJhaWxpbmdgIG9wdGlvbnMgYXJlIGB0cnVlYCwgYGZ1bmNgIGlzXG4gKiBpbnZva2VkIG9uIHRoZSB0cmFpbGluZyBlZGdlIG9mIHRoZSB0aW1lb3V0IG9ubHkgaWYgdGhlIHRocm90dGxlZCBmdW5jdGlvblxuICogaXMgaW52b2tlZCBtb3JlIHRoYW4gb25jZSBkdXJpbmcgdGhlIGB3YWl0YCB0aW1lb3V0LlxuICpcbiAqIElmIGB3YWl0YCBpcyBgMGAgYW5kIGBsZWFkaW5nYCBpcyBgZmFsc2VgLCBgZnVuY2AgaW52b2NhdGlvbiBpcyBkZWZlcnJlZFxuICogdW50aWwgdG8gdGhlIG5leHQgdGljaywgc2ltaWxhciB0byBgc2V0VGltZW91dGAgd2l0aCBhIHRpbWVvdXQgb2YgYDBgLlxuICpcbiAqIFNlZSBbRGF2aWQgQ29yYmFjaG8ncyBhcnRpY2xlXShodHRwczovL2Nzcy10cmlja3MuY29tL2RlYm91bmNpbmctdGhyb3R0bGluZy1leHBsYWluZWQtZXhhbXBsZXMvKVxuICogZm9yIGRldGFpbHMgb3ZlciB0aGUgZGlmZmVyZW5jZXMgYmV0d2VlbiBgXy50aHJvdHRsZWAgYW5kIGBfLmRlYm91bmNlYC5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDAuMS4wXG4gKiBAY2F0ZWdvcnkgRnVuY3Rpb25cbiAqIEBwYXJhbSB7RnVuY3Rpb259IGZ1bmMgVGhlIGZ1bmN0aW9uIHRvIHRocm90dGxlLlxuICogQHBhcmFtIHtudW1iZXJ9IFt3YWl0PTBdIFRoZSBudW1iZXIgb2YgbWlsbGlzZWNvbmRzIHRvIHRocm90dGxlIGludm9jYXRpb25zIHRvLlxuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zPXt9XSBUaGUgb3B0aW9ucyBvYmplY3QuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLmxlYWRpbmc9dHJ1ZV1cbiAqICBTcGVjaWZ5IGludm9raW5nIG9uIHRoZSBsZWFkaW5nIGVkZ2Ugb2YgdGhlIHRpbWVvdXQuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLnRyYWlsaW5nPXRydWVdXG4gKiAgU3BlY2lmeSBpbnZva2luZyBvbiB0aGUgdHJhaWxpbmcgZWRnZSBvZiB0aGUgdGltZW91dC5cbiAqIEByZXR1cm5zIHtGdW5jdGlvbn0gUmV0dXJucyB0aGUgbmV3IHRocm90dGxlZCBmdW5jdGlvbi5cbiAqIEBleGFtcGxlXG4gKlxuICogLy8gQXZvaWQgZXhjZXNzaXZlbHkgdXBkYXRpbmcgdGhlIHBvc2l0aW9uIHdoaWxlIHNjcm9sbGluZy5cbiAqIGpRdWVyeSh3aW5kb3cpLm9uKCdzY3JvbGwnLCBfLnRocm90dGxlKHVwZGF0ZVBvc2l0aW9uLCAxMDApKTtcbiAqXG4gKiAvLyBJbnZva2UgYHJlbmV3VG9rZW5gIHdoZW4gdGhlIGNsaWNrIGV2ZW50IGlzIGZpcmVkLCBidXQgbm90IG1vcmUgdGhhbiBvbmNlIGV2ZXJ5IDUgbWludXRlcy5cbiAqIHZhciB0aHJvdHRsZWQgPSBfLnRocm90dGxlKHJlbmV3VG9rZW4sIDMwMDAwMCwgeyAndHJhaWxpbmcnOiBmYWxzZSB9KTtcbiAqIGpRdWVyeShlbGVtZW50KS5vbignY2xpY2snLCB0aHJvdHRsZWQpO1xuICpcbiAqIC8vIENhbmNlbCB0aGUgdHJhaWxpbmcgdGhyb3R0bGVkIGludm9jYXRpb24uXG4gKiBqUXVlcnkod2luZG93KS5vbigncG9wc3RhdGUnLCB0aHJvdHRsZWQuY2FuY2VsKTtcbiAqL1xuZnVuY3Rpb24gdGhyb3R0bGUoZnVuYywgd2FpdCwgb3B0aW9ucykge1xuICB2YXIgbGVhZGluZyA9IHRydWUsXG4gICAgICB0cmFpbGluZyA9IHRydWU7XG5cbiAgaWYgKHR5cGVvZiBmdW5jICE9ICdmdW5jdGlvbicpIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKEZVTkNfRVJST1JfVEVYVCk7XG4gIH1cbiAgaWYgKGlzT2JqZWN0KG9wdGlvbnMpKSB7XG4gICAgbGVhZGluZyA9ICdsZWFkaW5nJyBpbiBvcHRpb25zID8gISFvcHRpb25zLmxlYWRpbmcgOiBsZWFkaW5nO1xuICAgIHRyYWlsaW5nID0gJ3RyYWlsaW5nJyBpbiBvcHRpb25zID8gISFvcHRpb25zLnRyYWlsaW5nIDogdHJhaWxpbmc7XG4gIH1cbiAgcmV0dXJuIGRlYm91bmNlKGZ1bmMsIHdhaXQsIHtcbiAgICAnbGVhZGluZyc6IGxlYWRpbmcsXG4gICAgJ21heFdhaXQnOiB3YWl0LFxuICAgICd0cmFpbGluZyc6IHRyYWlsaW5nXG4gIH0pO1xufVxuXG4vKipcbiAqIENoZWNrcyBpZiBgdmFsdWVgIGlzIHRoZVxuICogW2xhbmd1YWdlIHR5cGVdKGh0dHA6Ly93d3cuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi83LjAvI3NlYy1lY21hc2NyaXB0LWxhbmd1YWdlLXR5cGVzKVxuICogb2YgYE9iamVjdGAuIChlLmcuIGFycmF5cywgZnVuY3Rpb25zLCBvYmplY3RzLCByZWdleGVzLCBgbmV3IE51bWJlcigwKWAsIGFuZCBgbmV3IFN0cmluZygnJylgKVxuICpcbiAqIEBzdGF0aWNcbiAqIEBtZW1iZXJPZiBfXG4gKiBAc2luY2UgMC4xLjBcbiAqIEBjYXRlZ29yeSBMYW5nXG4gKiBAcGFyYW0geyp9IHZhbHVlIFRoZSB2YWx1ZSB0byBjaGVjay5cbiAqIEByZXR1cm5zIHtib29sZWFufSBSZXR1cm5zIGB0cnVlYCBpZiBgdmFsdWVgIGlzIGFuIG9iamVjdCwgZWxzZSBgZmFsc2VgLlxuICogQGV4YW1wbGVcbiAqXG4gKiBfLmlzT2JqZWN0KHt9KTtcbiAqIC8vID0+IHRydWVcbiAqXG4gKiBfLmlzT2JqZWN0KFsxLCAyLCAzXSk7XG4gKiAvLyA9PiB0cnVlXG4gKlxuICogXy5pc09iamVjdChfLm5vb3ApO1xuICogLy8gPT4gdHJ1ZVxuICpcbiAqIF8uaXNPYmplY3QobnVsbCk7XG4gKiAvLyA9PiBmYWxzZVxuICovXG5mdW5jdGlvbiBpc09iamVjdCh2YWx1ZSkge1xuICB2YXIgdHlwZSA9IHR5cGVvZiB2YWx1ZTtcbiAgcmV0dXJuICEhdmFsdWUgJiYgKHR5cGUgPT0gJ29iamVjdCcgfHwgdHlwZSA9PSAnZnVuY3Rpb24nKTtcbn1cblxuLyoqXG4gKiBDaGVja3MgaWYgYHZhbHVlYCBpcyBvYmplY3QtbGlrZS4gQSB2YWx1ZSBpcyBvYmplY3QtbGlrZSBpZiBpdCdzIG5vdCBgbnVsbGBcbiAqIGFuZCBoYXMgYSBgdHlwZW9mYCByZXN1bHQgb2YgXCJvYmplY3RcIi5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDQuMC4wXG4gKiBAY2F0ZWdvcnkgTGFuZ1xuICogQHBhcmFtIHsqfSB2YWx1ZSBUaGUgdmFsdWUgdG8gY2hlY2suXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gUmV0dXJucyBgdHJ1ZWAgaWYgYHZhbHVlYCBpcyBvYmplY3QtbGlrZSwgZWxzZSBgZmFsc2VgLlxuICogQGV4YW1wbGVcbiAqXG4gKiBfLmlzT2JqZWN0TGlrZSh7fSk7XG4gKiAvLyA9PiB0cnVlXG4gKlxuICogXy5pc09iamVjdExpa2UoWzEsIDIsIDNdKTtcbiAqIC8vID0+IHRydWVcbiAqXG4gKiBfLmlzT2JqZWN0TGlrZShfLm5vb3ApO1xuICogLy8gPT4gZmFsc2VcbiAqXG4gKiBfLmlzT2JqZWN0TGlrZShudWxsKTtcbiAqIC8vID0+IGZhbHNlXG4gKi9cbmZ1bmN0aW9uIGlzT2JqZWN0TGlrZSh2YWx1ZSkge1xuICByZXR1cm4gISF2YWx1ZSAmJiB0eXBlb2YgdmFsdWUgPT0gJ29iamVjdCc7XG59XG5cbi8qKlxuICogQ2hlY2tzIGlmIGB2YWx1ZWAgaXMgY2xhc3NpZmllZCBhcyBhIGBTeW1ib2xgIHByaW1pdGl2ZSBvciBvYmplY3QuXG4gKlxuICogQHN0YXRpY1xuICogQG1lbWJlck9mIF9cbiAqIEBzaW5jZSA0LjAuMFxuICogQGNhdGVnb3J5IExhbmdcbiAqIEBwYXJhbSB7Kn0gdmFsdWUgVGhlIHZhbHVlIHRvIGNoZWNrLlxuICogQHJldHVybnMge2Jvb2xlYW59IFJldHVybnMgYHRydWVgIGlmIGB2YWx1ZWAgaXMgYSBzeW1ib2wsIGVsc2UgYGZhbHNlYC5cbiAqIEBleGFtcGxlXG4gKlxuICogXy5pc1N5bWJvbChTeW1ib2wuaXRlcmF0b3IpO1xuICogLy8gPT4gdHJ1ZVxuICpcbiAqIF8uaXNTeW1ib2woJ2FiYycpO1xuICogLy8gPT4gZmFsc2VcbiAqL1xuZnVuY3Rpb24gaXNTeW1ib2wodmFsdWUpIHtcbiAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PSAnc3ltYm9sJyB8fFxuICAgIChpc09iamVjdExpa2UodmFsdWUpICYmIG9iamVjdFRvU3RyaW5nLmNhbGwodmFsdWUpID09IHN5bWJvbFRhZyk7XG59XG5cbi8qKlxuICogQ29udmVydHMgYHZhbHVlYCB0byBhIG51bWJlci5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDQuMC4wXG4gKiBAY2F0ZWdvcnkgTGFuZ1xuICogQHBhcmFtIHsqfSB2YWx1ZSBUaGUgdmFsdWUgdG8gcHJvY2Vzcy5cbiAqIEByZXR1cm5zIHtudW1iZXJ9IFJldHVybnMgdGhlIG51bWJlci5cbiAqIEBleGFtcGxlXG4gKlxuICogXy50b051bWJlcigzLjIpO1xuICogLy8gPT4gMy4yXG4gKlxuICogXy50b051bWJlcihOdW1iZXIuTUlOX1ZBTFVFKTtcbiAqIC8vID0+IDVlLTMyNFxuICpcbiAqIF8udG9OdW1iZXIoSW5maW5pdHkpO1xuICogLy8gPT4gSW5maW5pdHlcbiAqXG4gKiBfLnRvTnVtYmVyKCczLjInKTtcbiAqIC8vID0+IDMuMlxuICovXG5mdW5jdGlvbiB0b051bWJlcih2YWx1ZSkge1xuICBpZiAodHlwZW9mIHZhbHVlID09ICdudW1iZXInKSB7XG4gICAgcmV0dXJuIHZhbHVlO1xuICB9XG4gIGlmIChpc1N5bWJvbCh2YWx1ZSkpIHtcbiAgICByZXR1cm4gTkFOO1xuICB9XG4gIGlmIChpc09iamVjdCh2YWx1ZSkpIHtcbiAgICB2YXIgb3RoZXIgPSB0eXBlb2YgdmFsdWUudmFsdWVPZiA9PSAnZnVuY3Rpb24nID8gdmFsdWUudmFsdWVPZigpIDogdmFsdWU7XG4gICAgdmFsdWUgPSBpc09iamVjdChvdGhlcikgPyAob3RoZXIgKyAnJykgOiBvdGhlcjtcbiAgfVxuICBpZiAodHlwZW9mIHZhbHVlICE9ICdzdHJpbmcnKSB7XG4gICAgcmV0dXJuIHZhbHVlID09PSAwID8gdmFsdWUgOiArdmFsdWU7XG4gIH1cbiAgdmFsdWUgPSB2YWx1ZS5yZXBsYWNlKHJlVHJpbSwgJycpO1xuICB2YXIgaXNCaW5hcnkgPSByZUlzQmluYXJ5LnRlc3QodmFsdWUpO1xuICByZXR1cm4gKGlzQmluYXJ5IHx8IHJlSXNPY3RhbC50ZXN0KHZhbHVlKSlcbiAgICA/IGZyZWVQYXJzZUludCh2YWx1ZS5zbGljZSgyKSwgaXNCaW5hcnkgPyAyIDogOClcbiAgICA6IChyZUlzQmFkSGV4LnRlc3QodmFsdWUpID8gTkFOIDogK3ZhbHVlKTtcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB0aHJvdHRsZTtcbiIsInZhciBnO1xyXG5cclxuLy8gVGhpcyB3b3JrcyBpbiBub24tc3RyaWN0IG1vZGVcclxuZyA9IChmdW5jdGlvbigpIHtcclxuXHRyZXR1cm4gdGhpcztcclxufSkoKTtcclxuXHJcbnRyeSB7XHJcblx0Ly8gVGhpcyB3b3JrcyBpZiBldmFsIGlzIGFsbG93ZWQgKHNlZSBDU1ApXHJcblx0ZyA9IGcgfHwgRnVuY3Rpb24oXCJyZXR1cm4gdGhpc1wiKSgpIHx8ICgxLCBldmFsKShcInRoaXNcIik7XHJcbn0gY2F0Y2ggKGUpIHtcclxuXHQvLyBUaGlzIHdvcmtzIGlmIHRoZSB3aW5kb3cgcmVmZXJlbmNlIGlzIGF2YWlsYWJsZVxyXG5cdGlmICh0eXBlb2Ygd2luZG93ID09PSBcIm9iamVjdFwiKSBnID0gd2luZG93O1xyXG59XHJcblxyXG4vLyBnIGNhbiBzdGlsbCBiZSB1bmRlZmluZWQsIGJ1dCBub3RoaW5nIHRvIGRvIGFib3V0IGl0Li4uXHJcbi8vIFdlIHJldHVybiB1bmRlZmluZWQsIGluc3RlYWQgb2Ygbm90aGluZyBoZXJlLCBzbyBpdCdzXHJcbi8vIGVhc2llciB0byBoYW5kbGUgdGhpcyBjYXNlLiBpZighZ2xvYmFsKSB7IC4uLn1cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gZztcclxuIiwiLyotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbiAqICBDb3B5cmlnaHQgKGMpIE1pY3Jvc29mdCBDb3Jwb3JhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbiAqICBMaWNlbnNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuIFNlZSBMaWNlbnNlLnR4dCBpbiB0aGUgcHJvamVjdCByb290IGZvciBsaWNlbnNlIGluZm9ybWF0aW9uLlxuICotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSovXG5pbXBvcnQgeyBnZXRFbGVtZW50c0ZvclNvdXJjZUxpbmUgfSBmcm9tICcuL3Njcm9sbC1zeW5jJztcblxuZXhwb3J0IGNsYXNzIEFjdGl2ZUxpbmVNYXJrZXIge1xuXHRwcml2YXRlIF9jdXJyZW50OiBhbnk7XG5cblx0b25EaWRDaGFuZ2VUZXh0RWRpdG9yU2VsZWN0aW9uKGxpbmU6IG51bWJlcikge1xuXHRcdGNvbnN0IHsgcHJldmlvdXMgfSA9IGdldEVsZW1lbnRzRm9yU291cmNlTGluZShsaW5lKTtcblx0XHR0aGlzLl91cGRhdGUocHJldmlvdXMgJiYgcHJldmlvdXMuZWxlbWVudCk7XG5cdH1cblxuXHRfdXBkYXRlKGJlZm9yZTogSFRNTEVsZW1lbnQgfCB1bmRlZmluZWQpIHtcblx0XHR0aGlzLl91bm1hcmtBY3RpdmVFbGVtZW50KHRoaXMuX2N1cnJlbnQpO1xuXHRcdHRoaXMuX21hcmtBY3RpdmVFbGVtZW50KGJlZm9yZSk7XG5cdFx0dGhpcy5fY3VycmVudCA9IGJlZm9yZTtcblx0fVxuXG5cdF91bm1hcmtBY3RpdmVFbGVtZW50KGVsZW1lbnQ6IEhUTUxFbGVtZW50IHwgdW5kZWZpbmVkKSB7XG5cdFx0aWYgKCFlbGVtZW50KSB7XG5cdFx0XHRyZXR1cm47XG5cdFx0fVxuXHRcdGVsZW1lbnQuY2xhc3NOYW1lID0gZWxlbWVudC5jbGFzc05hbWUucmVwbGFjZSgvXFxiY29kZS1hY3RpdmUtbGluZVxcYi9nLCAnJyk7XG5cdH1cblxuXHRfbWFya0FjdGl2ZUVsZW1lbnQoZWxlbWVudDogSFRNTEVsZW1lbnQgfCB1bmRlZmluZWQpIHtcblx0XHRpZiAoIWVsZW1lbnQpIHtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cdFx0ZWxlbWVudC5jbGFzc05hbWUgKz0gJyBjb2RlLWFjdGl2ZS1saW5lJztcblx0fVxufSIsIi8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4gKiAgQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiAgTGljZW5zZWQgdW5kZXIgdGhlIE1JVCBMaWNlbnNlLiBTZWUgTGljZW5zZS50eHQgaW4gdGhlIHByb2plY3Qgcm9vdCBmb3IgbGljZW5zZSBpbmZvcm1hdGlvbi5cbiAqLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5leHBvcnQgZnVuY3Rpb24gb25jZURvY3VtZW50TG9hZGVkKGY6ICgpID0+IHZvaWQpIHtcblx0aWYgKGRvY3VtZW50LnJlYWR5U3RhdGUgPT09ICdsb2FkaW5nJyB8fCBkb2N1bWVudC5yZWFkeVN0YXRlIGFzIHN0cmluZyA9PT0gJ3VuaW5pdGlhbGl6ZWQnKSB7XG5cdFx0ZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignRE9NQ29udGVudExvYWRlZCcsIGYpO1xuXHR9IGVsc2Uge1xuXHRcdGYoKTtcblx0fVxufSIsIi8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4gKiAgQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiAgTGljZW5zZWQgdW5kZXIgdGhlIE1JVCBMaWNlbnNlLiBTZWUgTGljZW5zZS50eHQgaW4gdGhlIHByb2plY3Qgcm9vdCBmb3IgbGljZW5zZSBpbmZvcm1hdGlvbi5cbiAqLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5pbXBvcnQgeyBBY3RpdmVMaW5lTWFya2VyIH0gZnJvbSAnLi9hY3RpdmVMaW5lTWFya2VyJztcbmltcG9ydCB7IG9uY2VEb2N1bWVudExvYWRlZCB9IGZyb20gJy4vZXZlbnRzJztcbmltcG9ydCB7IGNyZWF0ZVBvc3RlckZvclZzQ29kZSB9IGZyb20gJy4vbWVzc2FnaW5nJztcbmltcG9ydCB7IGdldEVkaXRvckxpbmVOdW1iZXJGb3JQYWdlT2Zmc2V0LCBzY3JvbGxUb1JldmVhbFNvdXJjZUxpbmUgfSBmcm9tICcuL3Njcm9sbC1zeW5jJztcbmltcG9ydCB7IGdldFNldHRpbmdzLCBnZXREYXRhIH0gZnJvbSAnLi9zZXR0aW5ncyc7XG5pbXBvcnQgdGhyb3R0bGUgPSByZXF1aXJlKCdsb2Rhc2gudGhyb3R0bGUnKTtcblxuZGVjbGFyZSB2YXIgYWNxdWlyZVZzQ29kZUFwaTogYW55O1xuXG5sZXQgc2Nyb2xsRGlzYWJsZWQgPSB0cnVlO1xuY29uc3QgbWFya2VyID0gbmV3IEFjdGl2ZUxpbmVNYXJrZXIoKTtcbmNvbnN0IHNldHRpbmdzID0gZ2V0U2V0dGluZ3MoKTtcblxuY29uc3QgdnNjb2RlID0gYWNxdWlyZVZzQ29kZUFwaSgpO1xuXG4vLyBTZXQgVlMgQ29kZSBzdGF0ZVxubGV0IHN0YXRlID0gZ2V0RGF0YTx7IGxpbmU6IG51bWJlciB9PignZGF0YS1zdGF0ZScpO1xudnNjb2RlLnNldFN0YXRlKHN0YXRlKTtcblxuY29uc3QgbWVzc2FnaW5nID0gY3JlYXRlUG9zdGVyRm9yVnNDb2RlKHZzY29kZSk7XG5cbndpbmRvdy5jc3BBbGVydGVyLnNldFBvc3RlcihtZXNzYWdpbmcpO1xud2luZG93LnN0eWxlTG9hZGluZ01vbml0b3Iuc2V0UG9zdGVyKG1lc3NhZ2luZyk7XG5cbndpbmRvdy5vbmxvYWQgPSAoKSA9PiB7XG5cdHVwZGF0ZUltYWdlU2l6ZXMoKTtcbn07XG5cbm9uY2VEb2N1bWVudExvYWRlZCgoKSA9PiB7XG5cdGlmIChzZXR0aW5ncy5zY3JvbGxQcmV2aWV3V2l0aEVkaXRvcikge1xuXHRcdHNldFRpbWVvdXQoKCkgPT4ge1xuXHRcdFx0Y29uc3QgaW5pdGlhbExpbmUgPSArc2V0dGluZ3MubGluZTtcblx0XHRcdGlmICghaXNOYU4oaW5pdGlhbExpbmUpKSB7XG5cdFx0XHRcdHNjcm9sbERpc2FibGVkID0gdHJ1ZTtcblx0XHRcdFx0c2Nyb2xsVG9SZXZlYWxTb3VyY2VMaW5lKGluaXRpYWxMaW5lKTtcblx0XHRcdH1cblx0XHR9LCAwKTtcblx0fVxufSk7XG5cbmNvbnN0IG9uVXBkYXRlVmlldyA9ICgoKSA9PiB7XG5cdGNvbnN0IGRvU2Nyb2xsID0gdGhyb3R0bGUoKGxpbmU6IG51bWJlcikgPT4ge1xuXHRcdHNjcm9sbERpc2FibGVkID0gdHJ1ZTtcblx0XHRzY3JvbGxUb1JldmVhbFNvdXJjZUxpbmUobGluZSk7XG5cdH0sIDUwKTtcblxuXHRyZXR1cm4gKGxpbmU6IG51bWJlciwgc2V0dGluZ3M6IGFueSkgPT4ge1xuXHRcdGlmICghaXNOYU4obGluZSkpIHtcblx0XHRcdHNldHRpbmdzLmxpbmUgPSBsaW5lO1xuXHRcdFx0ZG9TY3JvbGwobGluZSk7XG5cdFx0fVxuXHR9O1xufSkoKTtcblxubGV0IHVwZGF0ZUltYWdlU2l6ZXMgPSB0aHJvdHRsZSgoKSA9PiB7XG5cdGNvbnN0IGltYWdlSW5mbzogeyBpZDogc3RyaW5nLCBoZWlnaHQ6IG51bWJlciwgd2lkdGg6IG51bWJlciB9W10gPSBbXTtcblx0bGV0IGltYWdlcyA9IGRvY3VtZW50LmdldEVsZW1lbnRzQnlUYWdOYW1lKCdpbWcnKTtcblx0aWYgKGltYWdlcykge1xuXHRcdGxldCBpO1xuXHRcdGZvciAoaSA9IDA7IGkgPCBpbWFnZXMubGVuZ3RoOyBpKyspIHtcblx0XHRcdGNvbnN0IGltZyA9IGltYWdlc1tpXTtcblxuXHRcdFx0aWYgKGltZy5jbGFzc0xpc3QuY29udGFpbnMoJ2xvYWRpbmcnKSkge1xuXHRcdFx0XHRpbWcuY2xhc3NMaXN0LnJlbW92ZSgnbG9hZGluZycpO1xuXHRcdFx0fVxuXG5cdFx0XHRpbWFnZUluZm8ucHVzaCh7XG5cdFx0XHRcdGlkOiBpbWcuaWQsXG5cdFx0XHRcdGhlaWdodDogaW1nLmhlaWdodCxcblx0XHRcdFx0d2lkdGg6IGltZy53aWR0aFxuXHRcdFx0fSk7XG5cdFx0fVxuXG5cdFx0bWVzc2FnaW5nLnBvc3RNZXNzYWdlKCdjYWNoZUltYWdlU2l6ZXMnLCBpbWFnZUluZm8pO1xuXHR9XG59LCA1MCk7XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdyZXNpemUnLCAoKSA9PiB7XG5cdHNjcm9sbERpc2FibGVkID0gdHJ1ZTtcblx0dXBkYXRlSW1hZ2VTaXplcygpO1xufSwgdHJ1ZSk7XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtZXNzYWdlJywgZXZlbnQgPT4ge1xuXHRpZiAoZXZlbnQuZGF0YS5zb3VyY2UgIT09IHNldHRpbmdzLnNvdXJjZSkge1xuXHRcdHJldHVybjtcblx0fVxuXG5cdHN3aXRjaCAoZXZlbnQuZGF0YS50eXBlKSB7XG5cdFx0Y2FzZSAnb25EaWRDaGFuZ2VUZXh0RWRpdG9yU2VsZWN0aW9uJzpcblx0XHRcdG1hcmtlci5vbkRpZENoYW5nZVRleHRFZGl0b3JTZWxlY3Rpb24oZXZlbnQuZGF0YS5saW5lKTtcblx0XHRcdGJyZWFrO1xuXG5cdFx0Y2FzZSAndXBkYXRlVmlldyc6XG5cdFx0XHRvblVwZGF0ZVZpZXcoZXZlbnQuZGF0YS5saW5lLCBzZXR0aW5ncyk7XG5cdFx0XHRicmVhaztcblx0fVxufSwgZmFsc2UpO1xuXG5kb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdkYmxjbGljaycsIGV2ZW50ID0+IHtcblx0aWYgKCFzZXR0aW5ncy5kb3VibGVDbGlja1RvU3dpdGNoVG9FZGl0b3IpIHtcblx0XHRyZXR1cm47XG5cdH1cblxuXHQvLyBJZ25vcmUgY2xpY2tzIG9uIGxpbmtzXG5cdGZvciAobGV0IG5vZGUgPSBldmVudC50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7IG5vZGU7IG5vZGUgPSBub2RlLnBhcmVudE5vZGUgYXMgSFRNTEVsZW1lbnQpIHtcblx0XHRpZiAobm9kZS50YWdOYW1lID09PSAnQScpIHtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cdH1cblxuXHRjb25zdCBvZmZzZXQgPSBldmVudC5wYWdlWTtcblx0Y29uc3QgbGluZSA9IGdldEVkaXRvckxpbmVOdW1iZXJGb3JQYWdlT2Zmc2V0KG9mZnNldCk7XG5cdGlmICh0eXBlb2YgbGluZSA9PT0gJ251bWJlcicgJiYgIWlzTmFOKGxpbmUpKSB7XG5cdFx0bWVzc2FnaW5nLnBvc3RNZXNzYWdlKCdkaWRDbGljaycsIHsgbGluZTogTWF0aC5mbG9vcihsaW5lKSB9KTtcblx0fVxufSk7XG5cbmRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZXZlbnQgPT4ge1xuXHRpZiAoIWV2ZW50KSB7XG5cdFx0cmV0dXJuO1xuXHR9XG5cblx0bGV0IG5vZGU6IGFueSA9IGV2ZW50LnRhcmdldDtcblx0d2hpbGUgKG5vZGUpIHtcblx0XHRpZiAobm9kZS50YWdOYW1lICYmIG5vZGUudGFnTmFtZSA9PT0gJ0EnICYmIG5vZGUuaHJlZikge1xuXHRcdFx0aWYgKG5vZGUuZ2V0QXR0cmlidXRlKCdocmVmJykuc3RhcnRzV2l0aCgnIycpKSB7XG5cdFx0XHRcdGJyZWFrO1xuXHRcdFx0fVxuXHRcdFx0aWYgKG5vZGUuaHJlZi5zdGFydHNXaXRoKCdmaWxlOi8vJykgfHwgbm9kZS5ocmVmLnN0YXJ0c1dpdGgoJ3ZzY29kZS1yZXNvdXJjZTonKSB8fCBub2RlLmhyZWYuc3RhcnRzV2l0aChzZXR0aW5ncy53ZWJ2aWV3UmVzb3VyY2VSb290KSkge1xuXHRcdFx0XHRjb25zdCBbcGF0aCwgZnJhZ21lbnRdID0gbm9kZS5ocmVmLnJlcGxhY2UoL14oZmlsZTpcXC9cXC98dnNjb2RlLXJlc291cmNlOikvaSwgJycpLnJlcGxhY2UobmV3IFJlZ0V4cChgXiR7ZXNjYXBlUmVnRXhwKHNldHRpbmdzLndlYnZpZXdSZXNvdXJjZVJvb3QpfWApKS5zcGxpdCgnIycpO1xuXHRcdFx0XHRtZXNzYWdpbmcucG9zdE1lc3NhZ2UoJ2NsaWNrTGluaycsIHsgcGF0aCwgZnJhZ21lbnQgfSk7XG5cdFx0XHRcdGV2ZW50LnByZXZlbnREZWZhdWx0KCk7XG5cdFx0XHRcdGV2ZW50LnN0b3BQcm9wYWdhdGlvbigpO1xuXHRcdFx0XHRicmVhaztcblx0XHRcdH1cblx0XHRcdGJyZWFrO1xuXHRcdH1cblx0XHRub2RlID0gbm9kZS5wYXJlbnROb2RlO1xuXHR9XG59LCB0cnVlKTtcblxuaWYgKHNldHRpbmdzLnNjcm9sbEVkaXRvcldpdGhQcmV2aWV3KSB7XG5cdHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdzY3JvbGwnLCB0aHJvdHRsZSgoKSA9PiB7XG5cdFx0aWYgKHNjcm9sbERpc2FibGVkKSB7XG5cdFx0XHRzY3JvbGxEaXNhYmxlZCA9IGZhbHNlO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRjb25zdCBsaW5lID0gZ2V0RWRpdG9yTGluZU51bWJlckZvclBhZ2VPZmZzZXQod2luZG93LnNjcm9sbFkpO1xuXHRcdFx0aWYgKHR5cGVvZiBsaW5lID09PSAnbnVtYmVyJyAmJiAhaXNOYU4obGluZSkpIHtcblx0XHRcdFx0bWVzc2FnaW5nLnBvc3RNZXNzYWdlKCdyZXZlYWxMaW5lJywgeyBsaW5lIH0pO1xuXHRcdFx0XHRzdGF0ZS5saW5lID0gbGluZTtcblx0XHRcdFx0dnNjb2RlLnNldFN0YXRlKHN0YXRlKTtcblx0XHRcdH1cblx0XHR9XG5cdH0sIDUwKSk7XG59XG5cbmZ1bmN0aW9uIGVzY2FwZVJlZ0V4cCh0ZXh0OiBzdHJpbmcpIHtcblx0cmV0dXJuIHRleHQucmVwbGFjZSgvWy1bXFxde30oKSorPy4sXFxcXF4kfCNcXHNdL2csICdcXFxcJCYnKTtcbn0iLCIvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuICogIENvcHlyaWdodCAoYykgTWljcm9zb2Z0IENvcnBvcmF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuICogIExpY2Vuc2VkIHVuZGVyIHRoZSBNSVQgTGljZW5zZS4gU2VlIExpY2Vuc2UudHh0IGluIHRoZSBwcm9qZWN0IHJvb3QgZm9yIGxpY2Vuc2UgaW5mb3JtYXRpb24uXG4gKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuaW1wb3J0IHsgZ2V0U2V0dGluZ3MgfSBmcm9tICcuL3NldHRpbmdzJztcblxuZXhwb3J0IGludGVyZmFjZSBNZXNzYWdlUG9zdGVyIHtcblx0LyoqXG5cdCAqIFBvc3QgYSBtZXNzYWdlIHRvIHRoZSBtYXJrZG93biBleHRlbnNpb25cblx0ICovXG5cdHBvc3RNZXNzYWdlKHR5cGU6IHN0cmluZywgYm9keTogb2JqZWN0KTogdm9pZDtcbn1cblxuZXhwb3J0IGNvbnN0IGNyZWF0ZVBvc3RlckZvclZzQ29kZSA9ICh2c2NvZGU6IGFueSkgPT4ge1xuXHRyZXR1cm4gbmV3IGNsYXNzIGltcGxlbWVudHMgTWVzc2FnZVBvc3RlciB7XG5cdFx0cG9zdE1lc3NhZ2UodHlwZTogc3RyaW5nLCBib2R5OiBvYmplY3QpOiB2b2lkIHtcblx0XHRcdHZzY29kZS5wb3N0TWVzc2FnZSh7XG5cdFx0XHRcdHR5cGUsXG5cdFx0XHRcdHNvdXJjZTogZ2V0U2V0dGluZ3MoKS5zb3VyY2UsXG5cdFx0XHRcdGJvZHlcblx0XHRcdH0pO1xuXHRcdH1cblx0fTtcbn07XG5cbiIsIi8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4gKiAgQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiAgTGljZW5zZWQgdW5kZXIgdGhlIE1JVCBMaWNlbnNlLiBTZWUgTGljZW5zZS50eHQgaW4gdGhlIHByb2plY3Qgcm9vdCBmb3IgbGljZW5zZSBpbmZvcm1hdGlvbi5cbiAqLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5pbXBvcnQgeyBnZXRTZXR0aW5ncyB9IGZyb20gJy4vc2V0dGluZ3MnO1xuXG5cbmZ1bmN0aW9uIGNsYW1wKG1pbjogbnVtYmVyLCBtYXg6IG51bWJlciwgdmFsdWU6IG51bWJlcikge1xuXHRyZXR1cm4gTWF0aC5taW4obWF4LCBNYXRoLm1heChtaW4sIHZhbHVlKSk7XG59XG5cbmZ1bmN0aW9uIGNsYW1wTGluZShsaW5lOiBudW1iZXIpIHtcblx0cmV0dXJuIGNsYW1wKDAsIGdldFNldHRpbmdzKCkubGluZUNvdW50IC0gMSwgbGluZSk7XG59XG5cblxuZXhwb3J0IGludGVyZmFjZSBDb2RlTGluZUVsZW1lbnQge1xuXHRlbGVtZW50OiBIVE1MRWxlbWVudDtcblx0bGluZTogbnVtYmVyO1xufVxuXG5jb25zdCBnZXRDb2RlTGluZUVsZW1lbnRzID0gKCgpID0+IHtcblx0bGV0IGVsZW1lbnRzOiBDb2RlTGluZUVsZW1lbnRbXTtcblx0cmV0dXJuICgpID0+IHtcblx0XHRpZiAoIWVsZW1lbnRzKSB7XG5cdFx0XHRlbGVtZW50cyA9IFt7IGVsZW1lbnQ6IGRvY3VtZW50LmJvZHksIGxpbmU6IDAgfV07XG5cdFx0XHRmb3IgKGNvbnN0IGVsZW1lbnQgb2YgZG9jdW1lbnQuZ2V0RWxlbWVudHNCeUNsYXNzTmFtZSgnY29kZS1saW5lJykpIHtcblx0XHRcdFx0Y29uc3QgbGluZSA9ICtlbGVtZW50LmdldEF0dHJpYnV0ZSgnZGF0YS1saW5lJykhO1xuXHRcdFx0XHRpZiAoIWlzTmFOKGxpbmUpKSB7XG5cdFx0XHRcdFx0ZWxlbWVudHMucHVzaCh7IGVsZW1lbnQ6IGVsZW1lbnQgYXMgSFRNTEVsZW1lbnQsIGxpbmUgfSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9XG5cdFx0cmV0dXJuIGVsZW1lbnRzO1xuXHR9O1xufSkoKTtcblxuLyoqXG4gKiBGaW5kIHRoZSBodG1sIGVsZW1lbnRzIHRoYXQgbWFwIHRvIGEgc3BlY2lmaWMgdGFyZ2V0IGxpbmUgaW4gdGhlIGVkaXRvci5cbiAqXG4gKiBJZiBhbiBleGFjdCBtYXRjaCwgcmV0dXJucyBhIHNpbmdsZSBlbGVtZW50LiBJZiB0aGUgbGluZSBpcyBiZXR3ZWVuIGVsZW1lbnRzLFxuICogcmV0dXJucyB0aGUgZWxlbWVudCBwcmlvciB0byBhbmQgdGhlIGVsZW1lbnQgYWZ0ZXIgdGhlIGdpdmVuIGxpbmUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZXRFbGVtZW50c0ZvclNvdXJjZUxpbmUodGFyZ2V0TGluZTogbnVtYmVyKTogeyBwcmV2aW91czogQ29kZUxpbmVFbGVtZW50OyBuZXh0PzogQ29kZUxpbmVFbGVtZW50OyB9IHtcblx0Y29uc3QgbGluZU51bWJlciA9IE1hdGguZmxvb3IodGFyZ2V0TGluZSk7XG5cdGNvbnN0IGxpbmVzID0gZ2V0Q29kZUxpbmVFbGVtZW50cygpO1xuXHRsZXQgcHJldmlvdXMgPSBsaW5lc1swXSB8fCBudWxsO1xuXHRmb3IgKGNvbnN0IGVudHJ5IG9mIGxpbmVzKSB7XG5cdFx0aWYgKGVudHJ5LmxpbmUgPT09IGxpbmVOdW1iZXIpIHtcblx0XHRcdHJldHVybiB7IHByZXZpb3VzOiBlbnRyeSwgbmV4dDogdW5kZWZpbmVkIH07XG5cdFx0fSBlbHNlIGlmIChlbnRyeS5saW5lID4gbGluZU51bWJlcikge1xuXHRcdFx0cmV0dXJuIHsgcHJldmlvdXMsIG5leHQ6IGVudHJ5IH07XG5cdFx0fVxuXHRcdHByZXZpb3VzID0gZW50cnk7XG5cdH1cblx0cmV0dXJuIHsgcHJldmlvdXMgfTtcbn1cblxuLyoqXG4gKiBGaW5kIHRoZSBodG1sIGVsZW1lbnRzIHRoYXQgYXJlIGF0IGEgc3BlY2lmaWMgcGl4ZWwgb2Zmc2V0IG9uIHRoZSBwYWdlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gZ2V0TGluZUVsZW1lbnRzQXRQYWdlT2Zmc2V0KG9mZnNldDogbnVtYmVyKTogeyBwcmV2aW91czogQ29kZUxpbmVFbGVtZW50OyBuZXh0PzogQ29kZUxpbmVFbGVtZW50OyB9IHtcblx0Y29uc3QgbGluZXMgPSBnZXRDb2RlTGluZUVsZW1lbnRzKCk7XG5cdGNvbnN0IHBvc2l0aW9uID0gb2Zmc2V0IC0gd2luZG93LnNjcm9sbFk7XG5cdGxldCBsbyA9IC0xO1xuXHRsZXQgaGkgPSBsaW5lcy5sZW5ndGggLSAxO1xuXHR3aGlsZSAobG8gKyAxIDwgaGkpIHtcblx0XHRjb25zdCBtaWQgPSBNYXRoLmZsb29yKChsbyArIGhpKSAvIDIpO1xuXHRcdGNvbnN0IGJvdW5kcyA9IGxpbmVzW21pZF0uZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcblx0XHRpZiAoYm91bmRzLnRvcCArIGJvdW5kcy5oZWlnaHQgPj0gcG9zaXRpb24pIHtcblx0XHRcdGhpID0gbWlkO1xuXHRcdH1cblx0XHRlbHNlIHtcblx0XHRcdGxvID0gbWlkO1xuXHRcdH1cblx0fVxuXHRjb25zdCBoaUVsZW1lbnQgPSBsaW5lc1toaV07XG5cdGNvbnN0IGhpQm91bmRzID0gaGlFbGVtZW50LmVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG5cdGlmIChoaSA+PSAxICYmIGhpQm91bmRzLnRvcCA+IHBvc2l0aW9uKSB7XG5cdFx0Y29uc3QgbG9FbGVtZW50ID0gbGluZXNbbG9dO1xuXHRcdHJldHVybiB7IHByZXZpb3VzOiBsb0VsZW1lbnQsIG5leHQ6IGhpRWxlbWVudCB9O1xuXHR9XG5cdHJldHVybiB7IHByZXZpb3VzOiBoaUVsZW1lbnQgfTtcbn1cblxuLyoqXG4gKiBBdHRlbXB0IHRvIHJldmVhbCB0aGUgZWxlbWVudCBmb3IgYSBzb3VyY2UgbGluZSBpbiB0aGUgZWRpdG9yLlxuICovXG5leHBvcnQgZnVuY3Rpb24gc2Nyb2xsVG9SZXZlYWxTb3VyY2VMaW5lKGxpbmU6IG51bWJlcikge1xuXHRpZiAoIWdldFNldHRpbmdzKCkuc2Nyb2xsUHJldmlld1dpdGhFZGl0b3IpIHtcblx0XHRyZXR1cm47XG5cdH1cblxuXHRpZiAobGluZSA8PSAwKSB7XG5cdFx0d2luZG93LnNjcm9sbCh3aW5kb3cuc2Nyb2xsWCwgMCk7XG5cdFx0cmV0dXJuO1xuXHR9XG5cblx0Y29uc3QgeyBwcmV2aW91cywgbmV4dCB9ID0gZ2V0RWxlbWVudHNGb3JTb3VyY2VMaW5lKGxpbmUpO1xuXHRpZiAoIXByZXZpb3VzKSB7XG5cdFx0cmV0dXJuO1xuXHR9XG5cdGxldCBzY3JvbGxUbyA9IDA7XG5cdGNvbnN0IHJlY3QgPSBwcmV2aW91cy5lbGVtZW50LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuXHRjb25zdCBwcmV2aW91c1RvcCA9IHJlY3QudG9wO1xuXHRpZiAobmV4dCAmJiBuZXh0LmxpbmUgIT09IHByZXZpb3VzLmxpbmUpIHtcblx0XHQvLyBCZXR3ZWVuIHR3byBlbGVtZW50cy4gR28gdG8gcGVyY2VudGFnZSBvZmZzZXQgYmV0d2VlbiB0aGVtLlxuXHRcdGNvbnN0IGJldHdlZW5Qcm9ncmVzcyA9IChsaW5lIC0gcHJldmlvdXMubGluZSkgLyAobmV4dC5saW5lIC0gcHJldmlvdXMubGluZSk7XG5cdFx0Y29uc3QgZWxlbWVudE9mZnNldCA9IG5leHQuZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKS50b3AgLSBwcmV2aW91c1RvcDtcblx0XHRzY3JvbGxUbyA9IHByZXZpb3VzVG9wICsgYmV0d2VlblByb2dyZXNzICogZWxlbWVudE9mZnNldDtcblx0fSBlbHNlIHtcblx0XHRjb25zdCBwcm9ncmVzc0luRWxlbWVudCA9IGxpbmUgLSBNYXRoLmZsb29yKGxpbmUpO1xuXHRcdHNjcm9sbFRvID0gcHJldmlvdXNUb3AgKyAocmVjdC5oZWlnaHQgKiBwcm9ncmVzc0luRWxlbWVudCk7XG5cdH1cblx0d2luZG93LnNjcm9sbCh3aW5kb3cuc2Nyb2xsWCwgTWF0aC5tYXgoMSwgd2luZG93LnNjcm9sbFkgKyBzY3JvbGxUbykpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gZ2V0RWRpdG9yTGluZU51bWJlckZvclBhZ2VPZmZzZXQob2Zmc2V0OiBudW1iZXIpIHtcblx0Y29uc3QgeyBwcmV2aW91cywgbmV4dCB9ID0gZ2V0TGluZUVsZW1lbnRzQXRQYWdlT2Zmc2V0KG9mZnNldCk7XG5cdGlmIChwcmV2aW91cykge1xuXHRcdGNvbnN0IHByZXZpb3VzQm91bmRzID0gcHJldmlvdXMuZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcblx0XHRjb25zdCBvZmZzZXRGcm9tUHJldmlvdXMgPSAob2Zmc2V0IC0gd2luZG93LnNjcm9sbFkgLSBwcmV2aW91c0JvdW5kcy50b3ApO1xuXHRcdGlmIChuZXh0KSB7XG5cdFx0XHRjb25zdCBwcm9ncmVzc0JldHdlZW5FbGVtZW50cyA9IG9mZnNldEZyb21QcmV2aW91cyAvIChuZXh0LmVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCkudG9wIC0gcHJldmlvdXNCb3VuZHMudG9wKTtcblx0XHRcdGNvbnN0IGxpbmUgPSBwcmV2aW91cy5saW5lICsgcHJvZ3Jlc3NCZXR3ZWVuRWxlbWVudHMgKiAobmV4dC5saW5lIC0gcHJldmlvdXMubGluZSk7XG5cdFx0XHRyZXR1cm4gY2xhbXBMaW5lKGxpbmUpO1xuXHRcdH1cblx0XHRlbHNlIHtcblx0XHRcdGNvbnN0IHByb2dyZXNzV2l0aGluRWxlbWVudCA9IG9mZnNldEZyb21QcmV2aW91cyAvIChwcmV2aW91c0JvdW5kcy5oZWlnaHQpO1xuXHRcdFx0Y29uc3QgbGluZSA9IHByZXZpb3VzLmxpbmUgKyBwcm9ncmVzc1dpdGhpbkVsZW1lbnQ7XG5cdFx0XHRyZXR1cm4gY2xhbXBMaW5lKGxpbmUpO1xuXHRcdH1cblx0fVxuXHRyZXR1cm4gbnVsbDtcbn1cbiIsIi8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4gKiAgQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiAgTGljZW5zZWQgdW5kZXIgdGhlIE1JVCBMaWNlbnNlLiBTZWUgTGljZW5zZS50eHQgaW4gdGhlIHByb2plY3Qgcm9vdCBmb3IgbGljZW5zZSBpbmZvcm1hdGlvbi5cbiAqLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5leHBvcnQgaW50ZXJmYWNlIFByZXZpZXdTZXR0aW5ncyB7XG5cdHJlYWRvbmx5IHNvdXJjZTogc3RyaW5nO1xuXHRyZWFkb25seSBsaW5lOiBudW1iZXI7XG5cdHJlYWRvbmx5IGxpbmVDb3VudDogbnVtYmVyO1xuXHRyZWFkb25seSBzY3JvbGxQcmV2aWV3V2l0aEVkaXRvcj86IGJvb2xlYW47XG5cdHJlYWRvbmx5IHNjcm9sbEVkaXRvcldpdGhQcmV2aWV3OiBib29sZWFuO1xuXHRyZWFkb25seSBkaXNhYmxlU2VjdXJpdHlXYXJuaW5nczogYm9vbGVhbjtcblx0cmVhZG9ubHkgZG91YmxlQ2xpY2tUb1N3aXRjaFRvRWRpdG9yOiBib29sZWFuO1xuXHRyZWFkb25seSB3ZWJ2aWV3UmVzb3VyY2VSb290OiBzdHJpbmc7XG59XG5cbmxldCBjYWNoZWRTZXR0aW5nczogUHJldmlld1NldHRpbmdzIHwgdW5kZWZpbmVkID0gdW5kZWZpbmVkO1xuXG5leHBvcnQgZnVuY3Rpb24gZ2V0RGF0YTxUID0ge30+KGtleTogc3RyaW5nKTogVCB7XG5cdGNvbnN0IGVsZW1lbnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgndnNjb2RlLW1hcmtkb3duLXByZXZpZXctZGF0YScpO1xuXHRpZiAoZWxlbWVudCkge1xuXHRcdGNvbnN0IGRhdGEgPSBlbGVtZW50LmdldEF0dHJpYnV0ZShrZXkpO1xuXHRcdGlmIChkYXRhKSB7XG5cdFx0XHRyZXR1cm4gSlNPTi5wYXJzZShkYXRhKTtcblx0XHR9XG5cdH1cblxuXHR0aHJvdyBuZXcgRXJyb3IoYENvdWxkIG5vdCBsb2FkIGRhdGEgZm9yICR7a2V5fWApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gZ2V0U2V0dGluZ3MoKTogUHJldmlld1NldHRpbmdzIHtcblx0aWYgKGNhY2hlZFNldHRpbmdzKSB7XG5cdFx0cmV0dXJuIGNhY2hlZFNldHRpbmdzO1xuXHR9XG5cblx0Y2FjaGVkU2V0dGluZ3MgPSBnZXREYXRhKCdkYXRhLXNldHRpbmdzJyk7XG5cdGlmIChjYWNoZWRTZXR0aW5ncykge1xuXHRcdHJldHVybiBjYWNoZWRTZXR0aW5ncztcblx0fVxuXG5cdHRocm93IG5ldyBFcnJvcignQ291bGQgbm90IGxvYWQgc2V0dGluZ3MnKTtcbn1cbiJdLCJzb3VyY2VSb290IjoiIn0= \ No newline at end of file +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vbm9kZV9tb2R1bGVzL2xvZGFzaC50aHJvdHRsZS9pbmRleC5qcyIsIndlYnBhY2s6Ly8vKHdlYnBhY2spL2J1aWxkaW4vZ2xvYmFsLmpzIiwid2VicGFjazovLy8uL3ByZXZpZXctc3JjL2FjdGl2ZUxpbmVNYXJrZXIudHMiLCJ3ZWJwYWNrOi8vLy4vcHJldmlldy1zcmMvZXZlbnRzLnRzIiwid2VicGFjazovLy8uL3ByZXZpZXctc3JjL2luZGV4LnRzIiwid2VicGFjazovLy8uL3ByZXZpZXctc3JjL21lc3NhZ2luZy50cyIsIndlYnBhY2s6Ly8vLi9wcmV2aWV3LXNyYy9zY3JvbGwtc3luYy50cyIsIndlYnBhY2s6Ly8vLi9wcmV2aWV3LXNyYy9zZXR0aW5ncy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7OztBQUdBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQUs7QUFDTDtBQUNBOztBQUVBO0FBQ0E7QUFDQSx5REFBaUQsY0FBYztBQUMvRDs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxtQ0FBMkIsMEJBQTBCLEVBQUU7QUFDdkQseUNBQWlDLGVBQWU7QUFDaEQ7QUFDQTtBQUNBOztBQUVBO0FBQ0EsOERBQXNELCtEQUErRDs7QUFFckg7QUFDQTs7O0FBR0E7QUFDQTs7Ozs7Ozs7Ozs7O0FDbkVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsYUFBYSxPQUFPO0FBQ3BCO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLFNBQVM7QUFDcEIsV0FBVyxPQUFPO0FBQ2xCLFdBQVcsT0FBTyxZQUFZO0FBQzlCLFdBQVcsUUFBUTtBQUNuQjtBQUNBLFdBQVcsT0FBTztBQUNsQjtBQUNBLFdBQVcsUUFBUTtBQUNuQjtBQUNBLGFBQWEsU0FBUztBQUN0QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBLDhDQUE4QyxrQkFBa0I7QUFDaEU7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLFNBQVM7QUFDcEIsV0FBVyxPQUFPO0FBQ2xCLFdBQVcsT0FBTyxZQUFZO0FBQzlCLFdBQVcsUUFBUTtBQUNuQjtBQUNBLFdBQVcsUUFBUTtBQUNuQjtBQUNBLGFBQWEsU0FBUztBQUN0QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxtREFBbUQsb0JBQW9CO0FBQ3ZFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSDs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLEVBQUU7QUFDYixhQUFhLFFBQVE7QUFDckI7QUFDQTtBQUNBLGdCQUFnQjtBQUNoQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFdBQVcsRUFBRTtBQUNiLGFBQWEsUUFBUTtBQUNyQjtBQUNBO0FBQ0Esb0JBQW9CO0FBQ3BCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXLEVBQUU7QUFDYixhQUFhLFFBQVE7QUFDckI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsV0FBVyxFQUFFO0FBQ2IsYUFBYSxPQUFPO0FBQ3BCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOzs7Ozs7Ozs7Ozs7O0FDdGJBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLENBQUM7O0FBRUQ7QUFDQTtBQUNBO0FBQ0EsQ0FBQztBQUNEO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0EsNENBQTRDOztBQUU1Qzs7Ozs7Ozs7Ozs7Ozs7O0FDbkJBOzs7Z0dBR2dHO0FBQ2hHLCtGQUF5RDtBQUV6RCxNQUFhLGdCQUFnQjtJQUc1Qiw4QkFBOEIsQ0FBQyxJQUFZO1FBQzFDLE1BQU0sRUFBRSxRQUFRLEVBQUUsR0FBRyxzQ0FBd0IsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNwRCxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsSUFBSSxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUVELE9BQU8sQ0FBQyxNQUErQjtRQUN0QyxJQUFJLENBQUMsb0JBQW9CLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3pDLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUNoQyxJQUFJLENBQUMsUUFBUSxHQUFHLE1BQU0sQ0FBQztJQUN4QixDQUFDO0lBRUQsb0JBQW9CLENBQUMsT0FBZ0M7UUFDcEQsSUFBSSxDQUFDLE9BQU8sRUFBRTtZQUNiLE9BQU87U0FDUDtRQUNELE9BQU8sQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsdUJBQXVCLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDNUUsQ0FBQztJQUVELGtCQUFrQixDQUFDLE9BQWdDO1FBQ2xELElBQUksQ0FBQyxPQUFPLEVBQUU7WUFDYixPQUFPO1NBQ1A7UUFDRCxPQUFPLENBQUMsU0FBUyxJQUFJLG1CQUFtQixDQUFDO0lBQzFDLENBQUM7Q0FDRDtBQTNCRCw0Q0EyQkM7Ozs7Ozs7Ozs7Ozs7O0FDakNEOzs7Z0dBR2dHOztBQUVoRyxTQUFnQixrQkFBa0IsQ0FBQyxDQUFhO0lBQy9DLElBQUksUUFBUSxDQUFDLFVBQVUsS0FBSyxTQUFTLElBQUksUUFBUSxDQUFDLFVBQW9CLEtBQUssZUFBZSxFQUFFO1FBQzNGLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxrQkFBa0IsRUFBRSxDQUFDLENBQUMsQ0FBQztLQUNqRDtTQUFNO1FBQ04sQ0FBQyxFQUFFLENBQUM7S0FDSjtBQUNGLENBQUM7QUFORCxnREFNQzs7Ozs7Ozs7Ozs7Ozs7QUNYRDs7O2dHQUdnRzs7QUFFaEcsOEdBQXNEO0FBQ3RELGdGQUE4QztBQUM5Qyx5RkFBb0Q7QUFDcEQsK0ZBQXNIO0FBQ3RILHNGQUFrRDtBQUNsRCx1R0FBNkM7QUFJN0MsSUFBSSxjQUFjLEdBQUcsSUFBSSxDQUFDO0FBQzFCLE1BQU0sTUFBTSxHQUFHLElBQUksbUNBQWdCLEVBQUUsQ0FBQztBQUN0QyxNQUFNLFFBQVEsR0FBRyxzQkFBVyxFQUFFLENBQUM7QUFFL0IsTUFBTSxNQUFNLEdBQUcsZ0JBQWdCLEVBQUUsQ0FBQztBQUVsQyxvQkFBb0I7QUFDcEIsSUFBSSxLQUFLLEdBQUcsa0JBQU8sQ0FBc0MsWUFBWSxDQUFDLENBQUM7QUFDdkUsTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUV2QixNQUFNLFNBQVMsR0FBRyxpQ0FBcUIsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUVoRCxNQUFNLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUN2QyxNQUFNLENBQUMsbUJBQW1CLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBRWhELE1BQU0sQ0FBQyxNQUFNLEdBQUcsR0FBRyxFQUFFO0lBQ3BCLGdCQUFnQixFQUFFLENBQUM7QUFDcEIsQ0FBQyxDQUFDO0FBRUYsMkJBQWtCLENBQUMsR0FBRyxFQUFFO0lBQ3ZCLElBQUksUUFBUSxDQUFDLHVCQUF1QixFQUFFO1FBQ3JDLFVBQVUsQ0FBQyxHQUFHLEVBQUU7WUFDZix5Q0FBeUM7WUFDekMsSUFBSSxLQUFLLENBQUMsUUFBUSxFQUFFO2dCQUNuQixNQUFNLE9BQU8sR0FBRyx1Q0FBeUIsQ0FBQyxLQUFLLENBQUMsUUFBUSxDQUFDLENBQUM7Z0JBQzFELElBQUksT0FBTyxFQUFFO29CQUNaLGNBQWMsR0FBRyxJQUFJLENBQUM7b0JBQ3RCLHNDQUF3QixDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztpQkFDdkM7YUFDRDtpQkFBTTtnQkFDTixNQUFNLFdBQVcsR0FBRyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUM7Z0JBQ25DLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLEVBQUU7b0JBQ3hCLGNBQWMsR0FBRyxJQUFJLENBQUM7b0JBQ3RCLHNDQUF3QixDQUFDLFdBQVcsQ0FBQyxDQUFDO2lCQUN0QzthQUNEO1FBQ0YsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO0tBQ047QUFDRixDQUFDLENBQUMsQ0FBQztBQUVILE1BQU0sWUFBWSxHQUFHLENBQUMsR0FBRyxFQUFFO0lBQzFCLE1BQU0sUUFBUSxHQUFHLFFBQVEsQ0FBQyxDQUFDLElBQVksRUFBRSxFQUFFO1FBQzFDLGNBQWMsR0FBRyxJQUFJLENBQUM7UUFDdEIsc0NBQXdCLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDaEMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBRVAsT0FBTyxDQUFDLElBQVksRUFBRSxRQUFhLEVBQUUsRUFBRTtRQUN0QyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxFQUFFO1lBQ2pCLFFBQVEsQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1lBQ3JCLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUNmO0lBQ0YsQ0FBQyxDQUFDO0FBQ0gsQ0FBQyxDQUFDLEVBQUUsQ0FBQztBQUVMLElBQUksZ0JBQWdCLEdBQUcsUUFBUSxDQUFDLEdBQUcsRUFBRTtJQUNwQyxNQUFNLFNBQVMsR0FBb0QsRUFBRSxDQUFDO0lBQ3RFLElBQUksTUFBTSxHQUFHLFFBQVEsQ0FBQyxvQkFBb0IsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNsRCxJQUFJLE1BQU0sRUFBRTtRQUNYLElBQUksQ0FBQyxDQUFDO1FBQ04sS0FBSyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFO1lBQ25DLE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUV0QixJQUFJLEdBQUcsQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxFQUFFO2dCQUN0QyxHQUFHLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQzthQUNoQztZQUVELFNBQVMsQ0FBQyxJQUFJLENBQUM7Z0JBQ2QsRUFBRSxFQUFFLEdBQUcsQ0FBQyxFQUFFO2dCQUNWLE1BQU0sRUFBRSxHQUFHLENBQUMsTUFBTTtnQkFDbEIsS0FBSyxFQUFFLEdBQUcsQ0FBQyxLQUFLO2FBQ2hCLENBQUMsQ0FBQztTQUNIO1FBRUQsU0FBUyxDQUFDLFdBQVcsQ0FBQyxpQkFBaUIsRUFBRSxTQUFTLENBQUMsQ0FBQztLQUNwRDtBQUNGLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztBQUVQLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxRQUFRLEVBQUUsR0FBRyxFQUFFO0lBQ3RDLGNBQWMsR0FBRyxJQUFJLENBQUM7SUFDdEIsZ0JBQWdCLEVBQUUsQ0FBQztBQUNwQixDQUFDLEVBQUUsSUFBSSxDQUFDLENBQUM7QUFFVCxNQUFNLENBQUMsZ0JBQWdCLENBQUMsU0FBUyxFQUFFLEtBQUssQ0FBQyxFQUFFO0lBQzFDLElBQUksS0FBSyxDQUFDLElBQUksQ0FBQyxNQUFNLEtBQUssUUFBUSxDQUFDLE1BQU0sRUFBRTtRQUMxQyxPQUFPO0tBQ1A7SUFFRCxRQUFRLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFO1FBQ3hCLEtBQUssZ0NBQWdDO1lBQ3BDLE1BQU0sQ0FBQyw4QkFBOEIsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ3ZELE1BQU07UUFFUCxLQUFLLFlBQVk7WUFDaEIsWUFBWSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1lBQ3hDLE1BQU07S0FDUDtBQUNGLENBQUMsRUFBRSxLQUFLLENBQUMsQ0FBQztBQUVWLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxVQUFVLEVBQUUsS0FBSyxDQUFDLEVBQUU7SUFDN0MsSUFBSSxDQUFDLFFBQVEsQ0FBQywyQkFBMkIsRUFBRTtRQUMxQyxPQUFPO0tBQ1A7SUFFRCx5QkFBeUI7SUFDekIsS0FBSyxJQUFJLElBQUksR0FBRyxLQUFLLENBQUMsTUFBcUIsRUFBRSxJQUFJLEVBQUUsSUFBSSxHQUFHLElBQUksQ0FBQyxVQUF5QixFQUFFO1FBQ3pGLElBQUksSUFBSSxDQUFDLE9BQU8sS0FBSyxHQUFHLEVBQUU7WUFDekIsT0FBTztTQUNQO0tBQ0Q7SUFFRCxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsS0FBSyxDQUFDO0lBQzNCLE1BQU0sSUFBSSxHQUFHLDhDQUFnQyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3RELElBQUksT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxFQUFFO1FBQzdDLFNBQVMsQ0FBQyxXQUFXLENBQUMsVUFBVSxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0tBQzlEO0FBQ0YsQ0FBQyxDQUFDLENBQUM7QUFFSCxRQUFRLENBQUMsZ0JBQWdCLENBQUMsT0FBTyxFQUFFLEtBQUssQ0FBQyxFQUFFO0lBQzFDLElBQUksQ0FBQyxLQUFLLEVBQUU7UUFDWCxPQUFPO0tBQ1A7SUFFRCxJQUFJLElBQUksR0FBUSxLQUFLLENBQUMsTUFBTSxDQUFDO0lBQzdCLE9BQU8sSUFBSSxFQUFFO1FBQ1osSUFBSSxJQUFJLENBQUMsT0FBTyxJQUFJLElBQUksQ0FBQyxPQUFPLEtBQUssR0FBRyxJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUU7WUFDdEQsSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsRUFBRTtnQkFDOUMsTUFBTTthQUNOO1lBQ0QsSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxtQkFBbUIsQ0FBQyxFQUFFO2dCQUN0SSxNQUFNLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLGdDQUFnQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxJQUFJLE1BQU0sQ0FBQyxJQUFJLFlBQVksQ0FBQyxRQUFRLENBQUMsbUJBQW1CLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7Z0JBQ2xLLFNBQVMsQ0FBQyxXQUFXLENBQUMsV0FBVyxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7Z0JBQ3ZELEtBQUssQ0FBQyxjQUFjLEVBQUUsQ0FBQztnQkFDdkIsS0FBSyxDQUFDLGVBQWUsRUFBRSxDQUFDO2dCQUN4QixNQUFNO2FBQ047WUFDRCxNQUFNO1NBQ047UUFDRCxJQUFJLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQztLQUN2QjtBQUNGLENBQUMsRUFBRSxJQUFJLENBQUMsQ0FBQztBQUVULElBQUksUUFBUSxDQUFDLHVCQUF1QixFQUFFO0lBQ3JDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxRQUFRLEVBQUUsUUFBUSxDQUFDLEdBQUcsRUFBRTtRQUMvQyxJQUFJLGNBQWMsRUFBRTtZQUNuQixjQUFjLEdBQUcsS0FBSyxDQUFDO1NBQ3ZCO2FBQU07WUFDTixNQUFNLElBQUksR0FBRyw4Q0FBZ0MsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUM7WUFDOUQsSUFBSSxPQUFPLElBQUksS0FBSyxRQUFRLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUU7Z0JBQzdDLFNBQVMsQ0FBQyxXQUFXLENBQUMsWUFBWSxFQUFFLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztnQkFDOUMsS0FBSyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7Z0JBQ2xCLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7YUFDdkI7U0FDRDtJQUNGLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO0NBQ1I7QUFFRCxTQUFTLFlBQVksQ0FBQyxJQUFZO0lBQ2pDLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQywwQkFBMEIsRUFBRSxNQUFNLENBQUMsQ0FBQztBQUN6RCxDQUFDOzs7Ozs7Ozs7Ozs7OztBQzVLRDs7O2dHQUdnRzs7QUFFaEcsc0ZBQXlDO0FBUzVCLDZCQUFxQixHQUFHLENBQUMsTUFBVyxFQUFFLEVBQUU7SUFDcEQsT0FBTyxJQUFJO1FBQ1YsV0FBVyxDQUFDLElBQVksRUFBRSxJQUFZO1lBQ3JDLE1BQU0sQ0FBQyxXQUFXLENBQUM7Z0JBQ2xCLElBQUk7Z0JBQ0osTUFBTSxFQUFFLHNCQUFXLEVBQUUsQ0FBQyxNQUFNO2dCQUM1QixJQUFJO2FBQ0osQ0FBQyxDQUFDO1FBQ0osQ0FBQztLQUNELENBQUM7QUFDSCxDQUFDLENBQUM7Ozs7Ozs7Ozs7Ozs7O0FDeEJGOzs7Z0dBR2dHOztBQUVoRyxzRkFBeUM7QUFHekMsU0FBUyxLQUFLLENBQUMsR0FBVyxFQUFFLEdBQVcsRUFBRSxLQUFhO0lBQ3JELE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQztBQUM1QyxDQUFDO0FBRUQsU0FBUyxTQUFTLENBQUMsSUFBWTtJQUM5QixPQUFPLEtBQUssQ0FBQyxDQUFDLEVBQUUsc0JBQVcsRUFBRSxDQUFDLFNBQVMsR0FBRyxDQUFDLEVBQUUsSUFBSSxDQUFDLENBQUM7QUFDcEQsQ0FBQztBQVFELE1BQU0sbUJBQW1CLEdBQUcsQ0FBQyxHQUFHLEVBQUU7SUFDakMsSUFBSSxRQUEyQixDQUFDO0lBQ2hDLE9BQU8sR0FBRyxFQUFFO1FBQ1gsSUFBSSxDQUFDLFFBQVEsRUFBRTtZQUNkLFFBQVEsR0FBRyxDQUFDLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQyxJQUFJLEVBQUUsSUFBSSxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7WUFDakQsS0FBSyxNQUFNLE9BQU8sSUFBSSxRQUFRLENBQUMsc0JBQXNCLENBQUMsV0FBVyxDQUFDLEVBQUU7Z0JBQ25FLE1BQU0sSUFBSSxHQUFHLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLENBQUUsQ0FBQztnQkFDakQsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRTtvQkFDakIsUUFBUSxDQUFDLElBQUksQ0FBQyxFQUFFLE9BQU8sRUFBRSxPQUFzQixFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7aUJBQ3pEO2FBQ0Q7U0FDRDtRQUNELE9BQU8sUUFBUSxDQUFDO0lBQ2pCLENBQUMsQ0FBQztBQUNILENBQUMsQ0FBQyxFQUFFLENBQUM7QUFFTDs7Ozs7R0FLRztBQUNILFNBQWdCLHdCQUF3QixDQUFDLFVBQWtCO0lBQzFELE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDMUMsTUFBTSxLQUFLLEdBQUcsbUJBQW1CLEVBQUUsQ0FBQztJQUNwQyxJQUFJLFFBQVEsR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDO0lBQ2hDLEtBQUssTUFBTSxLQUFLLElBQUksS0FBSyxFQUFFO1FBQzFCLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxVQUFVLEVBQUU7WUFDOUIsT0FBTyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFDO1NBQzVDO2FBQU0sSUFBSSxLQUFLLENBQUMsSUFBSSxHQUFHLFVBQVUsRUFBRTtZQUNuQyxPQUFPLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQztTQUNqQztRQUNELFFBQVEsR0FBRyxLQUFLLENBQUM7S0FDakI7SUFDRCxPQUFPLEVBQUUsUUFBUSxFQUFFLENBQUM7QUFDckIsQ0FBQztBQWJELDREQWFDO0FBRUQ7O0dBRUc7QUFDSCxTQUFnQiwyQkFBMkIsQ0FBQyxNQUFjO0lBQ3pELE1BQU0sS0FBSyxHQUFHLG1CQUFtQixFQUFFLENBQUM7SUFDcEMsTUFBTSxRQUFRLEdBQUcsTUFBTSxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUM7SUFDekMsSUFBSSxFQUFFLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDWixJQUFJLEVBQUUsR0FBRyxLQUFLLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQztJQUMxQixPQUFPLEVBQUUsR0FBRyxDQUFDLEdBQUcsRUFBRSxFQUFFO1FBQ25CLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDdEMsTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUFDO1FBQzFELElBQUksTUFBTSxDQUFDLEdBQUcsR0FBRyxNQUFNLENBQUMsTUFBTSxJQUFJLFFBQVEsRUFBRTtZQUMzQyxFQUFFLEdBQUcsR0FBRyxDQUFDO1NBQ1Q7YUFDSTtZQUNKLEVBQUUsR0FBRyxHQUFHLENBQUM7U0FDVDtLQUNEO0lBQ0QsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQzVCLE1BQU0sUUFBUSxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUMscUJBQXFCLEVBQUUsQ0FBQztJQUMzRCxJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUksUUFBUSxDQUFDLEdBQUcsR0FBRyxRQUFRLEVBQUU7UUFDdkMsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQzVCLE9BQU8sRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsQ0FBQztLQUNoRDtJQUNELE9BQU8sRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLENBQUM7QUFDaEMsQ0FBQztBQXRCRCxrRUFzQkM7QUFFRDs7R0FFRztBQUNILFNBQWdCLHdCQUF3QixDQUFDLElBQVk7SUFDcEQsSUFBSSxDQUFDLHNCQUFXLEVBQUUsQ0FBQyx1QkFBdUIsRUFBRTtRQUMzQyxPQUFPO0tBQ1A7SUFFRCxJQUFJLElBQUksSUFBSSxDQUFDLEVBQUU7UUFDZCxNQUFNLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUM7UUFDakMsT0FBTztLQUNQO0lBRUQsTUFBTSxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsR0FBRyx3QkFBd0IsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMxRCxJQUFJLENBQUMsUUFBUSxFQUFFO1FBQ2QsT0FBTztLQUNQO0lBQ0QsSUFBSSxRQUFRLEdBQUcsQ0FBQyxDQUFDO0lBQ2pCLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxPQUFPLENBQUMscUJBQXFCLEVBQUUsQ0FBQztJQUN0RCxNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDO0lBQzdCLElBQUksSUFBSSxJQUFJLElBQUksQ0FBQyxJQUFJLEtBQUssUUFBUSxDQUFDLElBQUksRUFBRTtRQUN4Qyw4REFBOEQ7UUFDOUQsTUFBTSxlQUFlLEdBQUcsQ0FBQyxJQUFJLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDN0UsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUFDLEdBQUcsR0FBRyxXQUFXLENBQUM7UUFDN0UsUUFBUSxHQUFHLFdBQVcsR0FBRyxlQUFlLEdBQUcsYUFBYSxDQUFDO0tBQ3pEO1NBQU07UUFDTixNQUFNLGlCQUFpQixHQUFHLElBQUksR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2xELFFBQVEsR0FBRyxXQUFXLEdBQUcsQ0FBQyxJQUFJLENBQUMsTUFBTSxHQUFHLGlCQUFpQixDQUFDLENBQUM7S0FDM0Q7SUFDRCxNQUFNLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsTUFBTSxDQUFDLE9BQU8sR0FBRyxRQUFRLENBQUMsQ0FBQyxDQUFDO0FBQ3ZFLENBQUM7QUEzQkQsNERBMkJDO0FBRUQsU0FBZ0IsZ0NBQWdDLENBQUMsTUFBYztJQUM5RCxNQUFNLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxHQUFHLDJCQUEyQixDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQy9ELElBQUksUUFBUSxFQUFFO1FBQ2IsTUFBTSxjQUFjLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUFDO1FBQ2hFLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLE9BQU8sR0FBRyxjQUFjLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDMUUsSUFBSSxJQUFJLEVBQUU7WUFDVCxNQUFNLHVCQUF1QixHQUFHLGtCQUFrQixHQUFHLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUFDLEdBQUcsR0FBRyxjQUFjLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDckgsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLElBQUksR0FBRyx1QkFBdUIsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ25GLE9BQU8sU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDO1NBQ3ZCO2FBQ0k7WUFDSixNQUFNLHFCQUFxQixHQUFHLGtCQUFrQixHQUFHLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQzNFLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxJQUFJLEdBQUcscUJBQXFCLENBQUM7WUFDbkQsT0FBTyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDdkI7S0FDRDtJQUNELE9BQU8sSUFBSSxDQUFDO0FBQ2IsQ0FBQztBQWpCRCw0RUFpQkM7QUFFRDs7R0FFRztBQUNILFNBQWdCLHlCQUF5QixDQUFDLFFBQWdCO0lBQ3pELE9BQU8sbUJBQW1CLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtRQUM3QyxPQUFPLE9BQU8sQ0FBQyxPQUFPLENBQUMsRUFBRSxLQUFLLFFBQVEsQ0FBQztJQUN4QyxDQUFDLENBQUMsQ0FBQztBQUNKLENBQUM7QUFKRCw4REFJQzs7Ozs7Ozs7Ozs7Ozs7QUNoSkQ7OztnR0FHZ0c7O0FBYWhHLElBQUksY0FBYyxHQUFnQyxTQUFTLENBQUM7QUFFNUQsU0FBZ0IsT0FBTyxDQUFTLEdBQVc7SUFDMUMsTUFBTSxPQUFPLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyw4QkFBOEIsQ0FBQyxDQUFDO0lBQ3hFLElBQUksT0FBTyxFQUFFO1FBQ1osTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUN2QyxJQUFJLElBQUksRUFBRTtZQUNULE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUN4QjtLQUNEO0lBRUQsTUFBTSxJQUFJLEtBQUssQ0FBQywyQkFBMkIsR0FBRyxFQUFFLENBQUMsQ0FBQztBQUNuRCxDQUFDO0FBVkQsMEJBVUM7QUFFRCxTQUFnQixXQUFXO0lBQzFCLElBQUksY0FBYyxFQUFFO1FBQ25CLE9BQU8sY0FBYyxDQUFDO0tBQ3RCO0lBRUQsY0FBYyxHQUFHLE9BQU8sQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUMxQyxJQUFJLGNBQWMsRUFBRTtRQUNuQixPQUFPLGNBQWMsQ0FBQztLQUN0QjtJQUVELE1BQU0sSUFBSSxLQUFLLENBQUMseUJBQXlCLENBQUMsQ0FBQztBQUM1QyxDQUFDO0FBWEQsa0NBV0MiLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VzQ29udGVudCI6WyIgXHQvLyBUaGUgbW9kdWxlIGNhY2hlXG4gXHR2YXIgaW5zdGFsbGVkTW9kdWxlcyA9IHt9O1xuXG4gXHQvLyBUaGUgcmVxdWlyZSBmdW5jdGlvblxuIFx0ZnVuY3Rpb24gX193ZWJwYWNrX3JlcXVpcmVfXyhtb2R1bGVJZCkge1xuXG4gXHRcdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuIFx0XHRpZihpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSkge1xuIFx0XHRcdHJldHVybiBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXS5leHBvcnRzO1xuIFx0XHR9XG4gXHRcdC8vIENyZWF0ZSBhIG5ldyBtb2R1bGUgKGFuZCBwdXQgaXQgaW50byB0aGUgY2FjaGUpXG4gXHRcdHZhciBtb2R1bGUgPSBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSA9IHtcbiBcdFx0XHRpOiBtb2R1bGVJZCxcbiBcdFx0XHRsOiBmYWxzZSxcbiBcdFx0XHRleHBvcnRzOiB7fVxuIFx0XHR9O1xuXG4gXHRcdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuIFx0XHRtb2R1bGVzW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuIFx0XHQvLyBGbGFnIHRoZSBtb2R1bGUgYXMgbG9hZGVkXG4gXHRcdG1vZHVsZS5sID0gdHJ1ZTtcblxuIFx0XHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuIFx0XHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG4gXHR9XG5cblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGVzIG9iamVjdCAoX193ZWJwYWNrX21vZHVsZXNfXylcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubSA9IG1vZHVsZXM7XG5cbiBcdC8vIGV4cG9zZSB0aGUgbW9kdWxlIGNhY2hlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmMgPSBpbnN0YWxsZWRNb2R1bGVzO1xuXG4gXHQvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9uIGZvciBoYXJtb255IGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uZCA9IGZ1bmN0aW9uKGV4cG9ydHMsIG5hbWUsIGdldHRlcikge1xuIFx0XHRpZighX193ZWJwYWNrX3JlcXVpcmVfXy5vKGV4cG9ydHMsIG5hbWUpKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIG5hbWUsIHtcbiBcdFx0XHRcdGNvbmZpZ3VyYWJsZTogZmFsc2UsXG4gXHRcdFx0XHRlbnVtZXJhYmxlOiB0cnVlLFxuIFx0XHRcdFx0Z2V0OiBnZXR0ZXJcbiBcdFx0XHR9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gZ2V0RGVmYXVsdEV4cG9ydCBmdW5jdGlvbiBmb3IgY29tcGF0aWJpbGl0eSB3aXRoIG5vbi1oYXJtb255IG1vZHVsZXNcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubiA9IGZ1bmN0aW9uKG1vZHVsZSkge1xuIFx0XHR2YXIgZ2V0dGVyID0gbW9kdWxlICYmIG1vZHVsZS5fX2VzTW9kdWxlID9cbiBcdFx0XHRmdW5jdGlvbiBnZXREZWZhdWx0KCkgeyByZXR1cm4gbW9kdWxlWydkZWZhdWx0J107IH0gOlxuIFx0XHRcdGZ1bmN0aW9uIGdldE1vZHVsZUV4cG9ydHMoKSB7IHJldHVybiBtb2R1bGU7IH07XG4gXHRcdF9fd2VicGFja19yZXF1aXJlX18uZChnZXR0ZXIsICdhJywgZ2V0dGVyKTtcbiBcdFx0cmV0dXJuIGdldHRlcjtcbiBcdH07XG5cbiBcdC8vIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbFxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5vID0gZnVuY3Rpb24ob2JqZWN0LCBwcm9wZXJ0eSkgeyByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKG9iamVjdCwgcHJvcGVydHkpOyB9O1xuXG4gXHQvLyBfX3dlYnBhY2tfcHVibGljX3BhdGhfX1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5wID0gXCJcIjtcblxuXG4gXHQvLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbiBcdHJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKF9fd2VicGFja19yZXF1aXJlX18ucyA9IFwiLi9wcmV2aWV3LXNyYy9pbmRleC50c1wiKTtcbiIsIi8qKlxuICogbG9kYXNoIChDdXN0b20gQnVpbGQpIDxodHRwczovL2xvZGFzaC5jb20vPlxuICogQnVpbGQ6IGBsb2Rhc2ggbW9kdWxhcml6ZSBleHBvcnRzPVwibnBtXCIgLW8gLi9gXG4gKiBDb3B5cmlnaHQgalF1ZXJ5IEZvdW5kYXRpb24gYW5kIG90aGVyIGNvbnRyaWJ1dG9ycyA8aHR0cHM6Ly9qcXVlcnkub3JnLz5cbiAqIFJlbGVhc2VkIHVuZGVyIE1JVCBsaWNlbnNlIDxodHRwczovL2xvZGFzaC5jb20vbGljZW5zZT5cbiAqIEJhc2VkIG9uIFVuZGVyc2NvcmUuanMgMS44LjMgPGh0dHA6Ly91bmRlcnNjb3JlanMub3JnL0xJQ0VOU0U+XG4gKiBDb3B5cmlnaHQgSmVyZW15IEFzaGtlbmFzLCBEb2N1bWVudENsb3VkIGFuZCBJbnZlc3RpZ2F0aXZlIFJlcG9ydGVycyAmIEVkaXRvcnNcbiAqL1xuXG4vKiogVXNlZCBhcyB0aGUgYFR5cGVFcnJvcmAgbWVzc2FnZSBmb3IgXCJGdW5jdGlvbnNcIiBtZXRob2RzLiAqL1xudmFyIEZVTkNfRVJST1JfVEVYVCA9ICdFeHBlY3RlZCBhIGZ1bmN0aW9uJztcblxuLyoqIFVzZWQgYXMgcmVmZXJlbmNlcyBmb3IgdmFyaW91cyBgTnVtYmVyYCBjb25zdGFudHMuICovXG52YXIgTkFOID0gMCAvIDA7XG5cbi8qKiBgT2JqZWN0I3RvU3RyaW5nYCByZXN1bHQgcmVmZXJlbmNlcy4gKi9cbnZhciBzeW1ib2xUYWcgPSAnW29iamVjdCBTeW1ib2xdJztcblxuLyoqIFVzZWQgdG8gbWF0Y2ggbGVhZGluZyBhbmQgdHJhaWxpbmcgd2hpdGVzcGFjZS4gKi9cbnZhciByZVRyaW0gPSAvXlxccyt8XFxzKyQvZztcblxuLyoqIFVzZWQgdG8gZGV0ZWN0IGJhZCBzaWduZWQgaGV4YWRlY2ltYWwgc3RyaW5nIHZhbHVlcy4gKi9cbnZhciByZUlzQmFkSGV4ID0gL15bLStdMHhbMC05YS1mXSskL2k7XG5cbi8qKiBVc2VkIHRvIGRldGVjdCBiaW5hcnkgc3RyaW5nIHZhbHVlcy4gKi9cbnZhciByZUlzQmluYXJ5ID0gL14wYlswMV0rJC9pO1xuXG4vKiogVXNlZCB0byBkZXRlY3Qgb2N0YWwgc3RyaW5nIHZhbHVlcy4gKi9cbnZhciByZUlzT2N0YWwgPSAvXjBvWzAtN10rJC9pO1xuXG4vKiogQnVpbHQtaW4gbWV0aG9kIHJlZmVyZW5jZXMgd2l0aG91dCBhIGRlcGVuZGVuY3kgb24gYHJvb3RgLiAqL1xudmFyIGZyZWVQYXJzZUludCA9IHBhcnNlSW50O1xuXG4vKiogRGV0ZWN0IGZyZWUgdmFyaWFibGUgYGdsb2JhbGAgZnJvbSBOb2RlLmpzLiAqL1xudmFyIGZyZWVHbG9iYWwgPSB0eXBlb2YgZ2xvYmFsID09ICdvYmplY3QnICYmIGdsb2JhbCAmJiBnbG9iYWwuT2JqZWN0ID09PSBPYmplY3QgJiYgZ2xvYmFsO1xuXG4vKiogRGV0ZWN0IGZyZWUgdmFyaWFibGUgYHNlbGZgLiAqL1xudmFyIGZyZWVTZWxmID0gdHlwZW9mIHNlbGYgPT0gJ29iamVjdCcgJiYgc2VsZiAmJiBzZWxmLk9iamVjdCA9PT0gT2JqZWN0ICYmIHNlbGY7XG5cbi8qKiBVc2VkIGFzIGEgcmVmZXJlbmNlIHRvIHRoZSBnbG9iYWwgb2JqZWN0LiAqL1xudmFyIHJvb3QgPSBmcmVlR2xvYmFsIHx8IGZyZWVTZWxmIHx8IEZ1bmN0aW9uKCdyZXR1cm4gdGhpcycpKCk7XG5cbi8qKiBVc2VkIGZvciBidWlsdC1pbiBtZXRob2QgcmVmZXJlbmNlcy4gKi9cbnZhciBvYmplY3RQcm90byA9IE9iamVjdC5wcm90b3R5cGU7XG5cbi8qKlxuICogVXNlZCB0byByZXNvbHZlIHRoZVxuICogW2B0b1N0cmluZ1RhZ2BdKGh0dHA6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzcuMC8jc2VjLW9iamVjdC5wcm90b3R5cGUudG9zdHJpbmcpXG4gKiBvZiB2YWx1ZXMuXG4gKi9cbnZhciBvYmplY3RUb1N0cmluZyA9IG9iamVjdFByb3RvLnRvU3RyaW5nO1xuXG4vKiBCdWlsdC1pbiBtZXRob2QgcmVmZXJlbmNlcyBmb3IgdGhvc2Ugd2l0aCB0aGUgc2FtZSBuYW1lIGFzIG90aGVyIGBsb2Rhc2hgIG1ldGhvZHMuICovXG52YXIgbmF0aXZlTWF4ID0gTWF0aC5tYXgsXG4gICAgbmF0aXZlTWluID0gTWF0aC5taW47XG5cbi8qKlxuICogR2V0cyB0aGUgdGltZXN0YW1wIG9mIHRoZSBudW1iZXIgb2YgbWlsbGlzZWNvbmRzIHRoYXQgaGF2ZSBlbGFwc2VkIHNpbmNlXG4gKiB0aGUgVW5peCBlcG9jaCAoMSBKYW51YXJ5IDE5NzAgMDA6MDA6MDAgVVRDKS5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDIuNC4wXG4gKiBAY2F0ZWdvcnkgRGF0ZVxuICogQHJldHVybnMge251bWJlcn0gUmV0dXJucyB0aGUgdGltZXN0YW1wLlxuICogQGV4YW1wbGVcbiAqXG4gKiBfLmRlZmVyKGZ1bmN0aW9uKHN0YW1wKSB7XG4gKiAgIGNvbnNvbGUubG9nKF8ubm93KCkgLSBzdGFtcCk7XG4gKiB9LCBfLm5vdygpKTtcbiAqIC8vID0+IExvZ3MgdGhlIG51bWJlciBvZiBtaWxsaXNlY29uZHMgaXQgdG9vayBmb3IgdGhlIGRlZmVycmVkIGludm9jYXRpb24uXG4gKi9cbnZhciBub3cgPSBmdW5jdGlvbigpIHtcbiAgcmV0dXJuIHJvb3QuRGF0ZS5ub3coKTtcbn07XG5cbi8qKlxuICogQ3JlYXRlcyBhIGRlYm91bmNlZCBmdW5jdGlvbiB0aGF0IGRlbGF5cyBpbnZva2luZyBgZnVuY2AgdW50aWwgYWZ0ZXIgYHdhaXRgXG4gKiBtaWxsaXNlY29uZHMgaGF2ZSBlbGFwc2VkIHNpbmNlIHRoZSBsYXN0IHRpbWUgdGhlIGRlYm91bmNlZCBmdW5jdGlvbiB3YXNcbiAqIGludm9rZWQuIFRoZSBkZWJvdW5jZWQgZnVuY3Rpb24gY29tZXMgd2l0aCBhIGBjYW5jZWxgIG1ldGhvZCB0byBjYW5jZWxcbiAqIGRlbGF5ZWQgYGZ1bmNgIGludm9jYXRpb25zIGFuZCBhIGBmbHVzaGAgbWV0aG9kIHRvIGltbWVkaWF0ZWx5IGludm9rZSB0aGVtLlxuICogUHJvdmlkZSBgb3B0aW9uc2AgdG8gaW5kaWNhdGUgd2hldGhlciBgZnVuY2Agc2hvdWxkIGJlIGludm9rZWQgb24gdGhlXG4gKiBsZWFkaW5nIGFuZC9vciB0cmFpbGluZyBlZGdlIG9mIHRoZSBgd2FpdGAgdGltZW91dC4gVGhlIGBmdW5jYCBpcyBpbnZva2VkXG4gKiB3aXRoIHRoZSBsYXN0IGFyZ3VtZW50cyBwcm92aWRlZCB0byB0aGUgZGVib3VuY2VkIGZ1bmN0aW9uLiBTdWJzZXF1ZW50XG4gKiBjYWxscyB0byB0aGUgZGVib3VuY2VkIGZ1bmN0aW9uIHJldHVybiB0aGUgcmVzdWx0IG9mIHRoZSBsYXN0IGBmdW5jYFxuICogaW52b2NhdGlvbi5cbiAqXG4gKiAqKk5vdGU6KiogSWYgYGxlYWRpbmdgIGFuZCBgdHJhaWxpbmdgIG9wdGlvbnMgYXJlIGB0cnVlYCwgYGZ1bmNgIGlzXG4gKiBpbnZva2VkIG9uIHRoZSB0cmFpbGluZyBlZGdlIG9mIHRoZSB0aW1lb3V0IG9ubHkgaWYgdGhlIGRlYm91bmNlZCBmdW5jdGlvblxuICogaXMgaW52b2tlZCBtb3JlIHRoYW4gb25jZSBkdXJpbmcgdGhlIGB3YWl0YCB0aW1lb3V0LlxuICpcbiAqIElmIGB3YWl0YCBpcyBgMGAgYW5kIGBsZWFkaW5nYCBpcyBgZmFsc2VgLCBgZnVuY2AgaW52b2NhdGlvbiBpcyBkZWZlcnJlZFxuICogdW50aWwgdG8gdGhlIG5leHQgdGljaywgc2ltaWxhciB0byBgc2V0VGltZW91dGAgd2l0aCBhIHRpbWVvdXQgb2YgYDBgLlxuICpcbiAqIFNlZSBbRGF2aWQgQ29yYmFjaG8ncyBhcnRpY2xlXShodHRwczovL2Nzcy10cmlja3MuY29tL2RlYm91bmNpbmctdGhyb3R0bGluZy1leHBsYWluZWQtZXhhbXBsZXMvKVxuICogZm9yIGRldGFpbHMgb3ZlciB0aGUgZGlmZmVyZW5jZXMgYmV0d2VlbiBgXy5kZWJvdW5jZWAgYW5kIGBfLnRocm90dGxlYC5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDAuMS4wXG4gKiBAY2F0ZWdvcnkgRnVuY3Rpb25cbiAqIEBwYXJhbSB7RnVuY3Rpb259IGZ1bmMgVGhlIGZ1bmN0aW9uIHRvIGRlYm91bmNlLlxuICogQHBhcmFtIHtudW1iZXJ9IFt3YWl0PTBdIFRoZSBudW1iZXIgb2YgbWlsbGlzZWNvbmRzIHRvIGRlbGF5LlxuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zPXt9XSBUaGUgb3B0aW9ucyBvYmplY3QuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLmxlYWRpbmc9ZmFsc2VdXG4gKiAgU3BlY2lmeSBpbnZva2luZyBvbiB0aGUgbGVhZGluZyBlZGdlIG9mIHRoZSB0aW1lb3V0LlxuICogQHBhcmFtIHtudW1iZXJ9IFtvcHRpb25zLm1heFdhaXRdXG4gKiAgVGhlIG1heGltdW0gdGltZSBgZnVuY2AgaXMgYWxsb3dlZCB0byBiZSBkZWxheWVkIGJlZm9yZSBpdCdzIGludm9rZWQuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLnRyYWlsaW5nPXRydWVdXG4gKiAgU3BlY2lmeSBpbnZva2luZyBvbiB0aGUgdHJhaWxpbmcgZWRnZSBvZiB0aGUgdGltZW91dC5cbiAqIEByZXR1cm5zIHtGdW5jdGlvbn0gUmV0dXJucyB0aGUgbmV3IGRlYm91bmNlZCBmdW5jdGlvbi5cbiAqIEBleGFtcGxlXG4gKlxuICogLy8gQXZvaWQgY29zdGx5IGNhbGN1bGF0aW9ucyB3aGlsZSB0aGUgd2luZG93IHNpemUgaXMgaW4gZmx1eC5cbiAqIGpRdWVyeSh3aW5kb3cpLm9uKCdyZXNpemUnLCBfLmRlYm91bmNlKGNhbGN1bGF0ZUxheW91dCwgMTUwKSk7XG4gKlxuICogLy8gSW52b2tlIGBzZW5kTWFpbGAgd2hlbiBjbGlja2VkLCBkZWJvdW5jaW5nIHN1YnNlcXVlbnQgY2FsbHMuXG4gKiBqUXVlcnkoZWxlbWVudCkub24oJ2NsaWNrJywgXy5kZWJvdW5jZShzZW5kTWFpbCwgMzAwLCB7XG4gKiAgICdsZWFkaW5nJzogdHJ1ZSxcbiAqICAgJ3RyYWlsaW5nJzogZmFsc2VcbiAqIH0pKTtcbiAqXG4gKiAvLyBFbnN1cmUgYGJhdGNoTG9nYCBpcyBpbnZva2VkIG9uY2UgYWZ0ZXIgMSBzZWNvbmQgb2YgZGVib3VuY2VkIGNhbGxzLlxuICogdmFyIGRlYm91bmNlZCA9IF8uZGVib3VuY2UoYmF0Y2hMb2csIDI1MCwgeyAnbWF4V2FpdCc6IDEwMDAgfSk7XG4gKiB2YXIgc291cmNlID0gbmV3IEV2ZW50U291cmNlKCcvc3RyZWFtJyk7XG4gKiBqUXVlcnkoc291cmNlKS5vbignbWVzc2FnZScsIGRlYm91bmNlZCk7XG4gKlxuICogLy8gQ2FuY2VsIHRoZSB0cmFpbGluZyBkZWJvdW5jZWQgaW52b2NhdGlvbi5cbiAqIGpRdWVyeSh3aW5kb3cpLm9uKCdwb3BzdGF0ZScsIGRlYm91bmNlZC5jYW5jZWwpO1xuICovXG5mdW5jdGlvbiBkZWJvdW5jZShmdW5jLCB3YWl0LCBvcHRpb25zKSB7XG4gIHZhciBsYXN0QXJncyxcbiAgICAgIGxhc3RUaGlzLFxuICAgICAgbWF4V2FpdCxcbiAgICAgIHJlc3VsdCxcbiAgICAgIHRpbWVySWQsXG4gICAgICBsYXN0Q2FsbFRpbWUsXG4gICAgICBsYXN0SW52b2tlVGltZSA9IDAsXG4gICAgICBsZWFkaW5nID0gZmFsc2UsXG4gICAgICBtYXhpbmcgPSBmYWxzZSxcbiAgICAgIHRyYWlsaW5nID0gdHJ1ZTtcblxuICBpZiAodHlwZW9mIGZ1bmMgIT0gJ2Z1bmN0aW9uJykge1xuICAgIHRocm93IG5ldyBUeXBlRXJyb3IoRlVOQ19FUlJPUl9URVhUKTtcbiAgfVxuICB3YWl0ID0gdG9OdW1iZXIod2FpdCkgfHwgMDtcbiAgaWYgKGlzT2JqZWN0KG9wdGlvbnMpKSB7XG4gICAgbGVhZGluZyA9ICEhb3B0aW9ucy5sZWFkaW5nO1xuICAgIG1heGluZyA9ICdtYXhXYWl0JyBpbiBvcHRpb25zO1xuICAgIG1heFdhaXQgPSBtYXhpbmcgPyBuYXRpdmVNYXgodG9OdW1iZXIob3B0aW9ucy5tYXhXYWl0KSB8fCAwLCB3YWl0KSA6IG1heFdhaXQ7XG4gICAgdHJhaWxpbmcgPSAndHJhaWxpbmcnIGluIG9wdGlvbnMgPyAhIW9wdGlvbnMudHJhaWxpbmcgOiB0cmFpbGluZztcbiAgfVxuXG4gIGZ1bmN0aW9uIGludm9rZUZ1bmModGltZSkge1xuICAgIHZhciBhcmdzID0gbGFzdEFyZ3MsXG4gICAgICAgIHRoaXNBcmcgPSBsYXN0VGhpcztcblxuICAgIGxhc3RBcmdzID0gbGFzdFRoaXMgPSB1bmRlZmluZWQ7XG4gICAgbGFzdEludm9rZVRpbWUgPSB0aW1lO1xuICAgIHJlc3VsdCA9IGZ1bmMuYXBwbHkodGhpc0FyZywgYXJncyk7XG4gICAgcmV0dXJuIHJlc3VsdDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGxlYWRpbmdFZGdlKHRpbWUpIHtcbiAgICAvLyBSZXNldCBhbnkgYG1heFdhaXRgIHRpbWVyLlxuICAgIGxhc3RJbnZva2VUaW1lID0gdGltZTtcbiAgICAvLyBTdGFydCB0aGUgdGltZXIgZm9yIHRoZSB0cmFpbGluZyBlZGdlLlxuICAgIHRpbWVySWQgPSBzZXRUaW1lb3V0KHRpbWVyRXhwaXJlZCwgd2FpdCk7XG4gICAgLy8gSW52b2tlIHRoZSBsZWFkaW5nIGVkZ2UuXG4gICAgcmV0dXJuIGxlYWRpbmcgPyBpbnZva2VGdW5jKHRpbWUpIDogcmVzdWx0O1xuICB9XG5cbiAgZnVuY3Rpb24gcmVtYWluaW5nV2FpdCh0aW1lKSB7XG4gICAgdmFyIHRpbWVTaW5jZUxhc3RDYWxsID0gdGltZSAtIGxhc3RDYWxsVGltZSxcbiAgICAgICAgdGltZVNpbmNlTGFzdEludm9rZSA9IHRpbWUgLSBsYXN0SW52b2tlVGltZSxcbiAgICAgICAgcmVzdWx0ID0gd2FpdCAtIHRpbWVTaW5jZUxhc3RDYWxsO1xuXG4gICAgcmV0dXJuIG1heGluZyA/IG5hdGl2ZU1pbihyZXN1bHQsIG1heFdhaXQgLSB0aW1lU2luY2VMYXN0SW52b2tlKSA6IHJlc3VsdDtcbiAgfVxuXG4gIGZ1bmN0aW9uIHNob3VsZEludm9rZSh0aW1lKSB7XG4gICAgdmFyIHRpbWVTaW5jZUxhc3RDYWxsID0gdGltZSAtIGxhc3RDYWxsVGltZSxcbiAgICAgICAgdGltZVNpbmNlTGFzdEludm9rZSA9IHRpbWUgLSBsYXN0SW52b2tlVGltZTtcblxuICAgIC8vIEVpdGhlciB0aGlzIGlzIHRoZSBmaXJzdCBjYWxsLCBhY3Rpdml0eSBoYXMgc3RvcHBlZCBhbmQgd2UncmUgYXQgdGhlXG4gICAgLy8gdHJhaWxpbmcgZWRnZSwgdGhlIHN5c3RlbSB0aW1lIGhhcyBnb25lIGJhY2t3YXJkcyBhbmQgd2UncmUgdHJlYXRpbmdcbiAgICAvLyBpdCBhcyB0aGUgdHJhaWxpbmcgZWRnZSwgb3Igd2UndmUgaGl0IHRoZSBgbWF4V2FpdGAgbGltaXQuXG4gICAgcmV0dXJuIChsYXN0Q2FsbFRpbWUgPT09IHVuZGVmaW5lZCB8fCAodGltZVNpbmNlTGFzdENhbGwgPj0gd2FpdCkgfHxcbiAgICAgICh0aW1lU2luY2VMYXN0Q2FsbCA8IDApIHx8IChtYXhpbmcgJiYgdGltZVNpbmNlTGFzdEludm9rZSA+PSBtYXhXYWl0KSk7XG4gIH1cblxuICBmdW5jdGlvbiB0aW1lckV4cGlyZWQoKSB7XG4gICAgdmFyIHRpbWUgPSBub3coKTtcbiAgICBpZiAoc2hvdWxkSW52b2tlKHRpbWUpKSB7XG4gICAgICByZXR1cm4gdHJhaWxpbmdFZGdlKHRpbWUpO1xuICAgIH1cbiAgICAvLyBSZXN0YXJ0IHRoZSB0aW1lci5cbiAgICB0aW1lcklkID0gc2V0VGltZW91dCh0aW1lckV4cGlyZWQsIHJlbWFpbmluZ1dhaXQodGltZSkpO1xuICB9XG5cbiAgZnVuY3Rpb24gdHJhaWxpbmdFZGdlKHRpbWUpIHtcbiAgICB0aW1lcklkID0gdW5kZWZpbmVkO1xuXG4gICAgLy8gT25seSBpbnZva2UgaWYgd2UgaGF2ZSBgbGFzdEFyZ3NgIHdoaWNoIG1lYW5zIGBmdW5jYCBoYXMgYmVlblxuICAgIC8vIGRlYm91bmNlZCBhdCBsZWFzdCBvbmNlLlxuICAgIGlmICh0cmFpbGluZyAmJiBsYXN0QXJncykge1xuICAgICAgcmV0dXJuIGludm9rZUZ1bmModGltZSk7XG4gICAgfVxuICAgIGxhc3RBcmdzID0gbGFzdFRoaXMgPSB1bmRlZmluZWQ7XG4gICAgcmV0dXJuIHJlc3VsdDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGNhbmNlbCgpIHtcbiAgICBpZiAodGltZXJJZCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICBjbGVhclRpbWVvdXQodGltZXJJZCk7XG4gICAgfVxuICAgIGxhc3RJbnZva2VUaW1lID0gMDtcbiAgICBsYXN0QXJncyA9IGxhc3RDYWxsVGltZSA9IGxhc3RUaGlzID0gdGltZXJJZCA9IHVuZGVmaW5lZDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGZsdXNoKCkge1xuICAgIHJldHVybiB0aW1lcklkID09PSB1bmRlZmluZWQgPyByZXN1bHQgOiB0cmFpbGluZ0VkZ2Uobm93KCkpO1xuICB9XG5cbiAgZnVuY3Rpb24gZGVib3VuY2VkKCkge1xuICAgIHZhciB0aW1lID0gbm93KCksXG4gICAgICAgIGlzSW52b2tpbmcgPSBzaG91bGRJbnZva2UodGltZSk7XG5cbiAgICBsYXN0QXJncyA9IGFyZ3VtZW50cztcbiAgICBsYXN0VGhpcyA9IHRoaXM7XG4gICAgbGFzdENhbGxUaW1lID0gdGltZTtcblxuICAgIGlmIChpc0ludm9raW5nKSB7XG4gICAgICBpZiAodGltZXJJZCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBsZWFkaW5nRWRnZShsYXN0Q2FsbFRpbWUpO1xuICAgICAgfVxuICAgICAgaWYgKG1heGluZykge1xuICAgICAgICAvLyBIYW5kbGUgaW52b2NhdGlvbnMgaW4gYSB0aWdodCBsb29wLlxuICAgICAgICB0aW1lcklkID0gc2V0VGltZW91dCh0aW1lckV4cGlyZWQsIHdhaXQpO1xuICAgICAgICByZXR1cm4gaW52b2tlRnVuYyhsYXN0Q2FsbFRpbWUpO1xuICAgICAgfVxuICAgIH1cbiAgICBpZiAodGltZXJJZCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICB0aW1lcklkID0gc2V0VGltZW91dCh0aW1lckV4cGlyZWQsIHdhaXQpO1xuICAgIH1cbiAgICByZXR1cm4gcmVzdWx0O1xuICB9XG4gIGRlYm91bmNlZC5jYW5jZWwgPSBjYW5jZWw7XG4gIGRlYm91bmNlZC5mbHVzaCA9IGZsdXNoO1xuICByZXR1cm4gZGVib3VuY2VkO1xufVxuXG4vKipcbiAqIENyZWF0ZXMgYSB0aHJvdHRsZWQgZnVuY3Rpb24gdGhhdCBvbmx5IGludm9rZXMgYGZ1bmNgIGF0IG1vc3Qgb25jZSBwZXJcbiAqIGV2ZXJ5IGB3YWl0YCBtaWxsaXNlY29uZHMuIFRoZSB0aHJvdHRsZWQgZnVuY3Rpb24gY29tZXMgd2l0aCBhIGBjYW5jZWxgXG4gKiBtZXRob2QgdG8gY2FuY2VsIGRlbGF5ZWQgYGZ1bmNgIGludm9jYXRpb25zIGFuZCBhIGBmbHVzaGAgbWV0aG9kIHRvXG4gKiBpbW1lZGlhdGVseSBpbnZva2UgdGhlbS4gUHJvdmlkZSBgb3B0aW9uc2AgdG8gaW5kaWNhdGUgd2hldGhlciBgZnVuY2BcbiAqIHNob3VsZCBiZSBpbnZva2VkIG9uIHRoZSBsZWFkaW5nIGFuZC9vciB0cmFpbGluZyBlZGdlIG9mIHRoZSBgd2FpdGBcbiAqIHRpbWVvdXQuIFRoZSBgZnVuY2AgaXMgaW52b2tlZCB3aXRoIHRoZSBsYXN0IGFyZ3VtZW50cyBwcm92aWRlZCB0byB0aGVcbiAqIHRocm90dGxlZCBmdW5jdGlvbi4gU3Vic2VxdWVudCBjYWxscyB0byB0aGUgdGhyb3R0bGVkIGZ1bmN0aW9uIHJldHVybiB0aGVcbiAqIHJlc3VsdCBvZiB0aGUgbGFzdCBgZnVuY2AgaW52b2NhdGlvbi5cbiAqXG4gKiAqKk5vdGU6KiogSWYgYGxlYWRpbmdgIGFuZCBgdHJhaWxpbmdgIG9wdGlvbnMgYXJlIGB0cnVlYCwgYGZ1bmNgIGlzXG4gKiBpbnZva2VkIG9uIHRoZSB0cmFpbGluZyBlZGdlIG9mIHRoZSB0aW1lb3V0IG9ubHkgaWYgdGhlIHRocm90dGxlZCBmdW5jdGlvblxuICogaXMgaW52b2tlZCBtb3JlIHRoYW4gb25jZSBkdXJpbmcgdGhlIGB3YWl0YCB0aW1lb3V0LlxuICpcbiAqIElmIGB3YWl0YCBpcyBgMGAgYW5kIGBsZWFkaW5nYCBpcyBgZmFsc2VgLCBgZnVuY2AgaW52b2NhdGlvbiBpcyBkZWZlcnJlZFxuICogdW50aWwgdG8gdGhlIG5leHQgdGljaywgc2ltaWxhciB0byBgc2V0VGltZW91dGAgd2l0aCBhIHRpbWVvdXQgb2YgYDBgLlxuICpcbiAqIFNlZSBbRGF2aWQgQ29yYmFjaG8ncyBhcnRpY2xlXShodHRwczovL2Nzcy10cmlja3MuY29tL2RlYm91bmNpbmctdGhyb3R0bGluZy1leHBsYWluZWQtZXhhbXBsZXMvKVxuICogZm9yIGRldGFpbHMgb3ZlciB0aGUgZGlmZmVyZW5jZXMgYmV0d2VlbiBgXy50aHJvdHRsZWAgYW5kIGBfLmRlYm91bmNlYC5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDAuMS4wXG4gKiBAY2F0ZWdvcnkgRnVuY3Rpb25cbiAqIEBwYXJhbSB7RnVuY3Rpb259IGZ1bmMgVGhlIGZ1bmN0aW9uIHRvIHRocm90dGxlLlxuICogQHBhcmFtIHtudW1iZXJ9IFt3YWl0PTBdIFRoZSBudW1iZXIgb2YgbWlsbGlzZWNvbmRzIHRvIHRocm90dGxlIGludm9jYXRpb25zIHRvLlxuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zPXt9XSBUaGUgb3B0aW9ucyBvYmplY3QuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLmxlYWRpbmc9dHJ1ZV1cbiAqICBTcGVjaWZ5IGludm9raW5nIG9uIHRoZSBsZWFkaW5nIGVkZ2Ugb2YgdGhlIHRpbWVvdXQuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtvcHRpb25zLnRyYWlsaW5nPXRydWVdXG4gKiAgU3BlY2lmeSBpbnZva2luZyBvbiB0aGUgdHJhaWxpbmcgZWRnZSBvZiB0aGUgdGltZW91dC5cbiAqIEByZXR1cm5zIHtGdW5jdGlvbn0gUmV0dXJucyB0aGUgbmV3IHRocm90dGxlZCBmdW5jdGlvbi5cbiAqIEBleGFtcGxlXG4gKlxuICogLy8gQXZvaWQgZXhjZXNzaXZlbHkgdXBkYXRpbmcgdGhlIHBvc2l0aW9uIHdoaWxlIHNjcm9sbGluZy5cbiAqIGpRdWVyeSh3aW5kb3cpLm9uKCdzY3JvbGwnLCBfLnRocm90dGxlKHVwZGF0ZVBvc2l0aW9uLCAxMDApKTtcbiAqXG4gKiAvLyBJbnZva2UgYHJlbmV3VG9rZW5gIHdoZW4gdGhlIGNsaWNrIGV2ZW50IGlzIGZpcmVkLCBidXQgbm90IG1vcmUgdGhhbiBvbmNlIGV2ZXJ5IDUgbWludXRlcy5cbiAqIHZhciB0aHJvdHRsZWQgPSBfLnRocm90dGxlKHJlbmV3VG9rZW4sIDMwMDAwMCwgeyAndHJhaWxpbmcnOiBmYWxzZSB9KTtcbiAqIGpRdWVyeShlbGVtZW50KS5vbignY2xpY2snLCB0aHJvdHRsZWQpO1xuICpcbiAqIC8vIENhbmNlbCB0aGUgdHJhaWxpbmcgdGhyb3R0bGVkIGludm9jYXRpb24uXG4gKiBqUXVlcnkod2luZG93KS5vbigncG9wc3RhdGUnLCB0aHJvdHRsZWQuY2FuY2VsKTtcbiAqL1xuZnVuY3Rpb24gdGhyb3R0bGUoZnVuYywgd2FpdCwgb3B0aW9ucykge1xuICB2YXIgbGVhZGluZyA9IHRydWUsXG4gICAgICB0cmFpbGluZyA9IHRydWU7XG5cbiAgaWYgKHR5cGVvZiBmdW5jICE9ICdmdW5jdGlvbicpIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKEZVTkNfRVJST1JfVEVYVCk7XG4gIH1cbiAgaWYgKGlzT2JqZWN0KG9wdGlvbnMpKSB7XG4gICAgbGVhZGluZyA9ICdsZWFkaW5nJyBpbiBvcHRpb25zID8gISFvcHRpb25zLmxlYWRpbmcgOiBsZWFkaW5nO1xuICAgIHRyYWlsaW5nID0gJ3RyYWlsaW5nJyBpbiBvcHRpb25zID8gISFvcHRpb25zLnRyYWlsaW5nIDogdHJhaWxpbmc7XG4gIH1cbiAgcmV0dXJuIGRlYm91bmNlKGZ1bmMsIHdhaXQsIHtcbiAgICAnbGVhZGluZyc6IGxlYWRpbmcsXG4gICAgJ21heFdhaXQnOiB3YWl0LFxuICAgICd0cmFpbGluZyc6IHRyYWlsaW5nXG4gIH0pO1xufVxuXG4vKipcbiAqIENoZWNrcyBpZiBgdmFsdWVgIGlzIHRoZVxuICogW2xhbmd1YWdlIHR5cGVdKGh0dHA6Ly93d3cuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi83LjAvI3NlYy1lY21hc2NyaXB0LWxhbmd1YWdlLXR5cGVzKVxuICogb2YgYE9iamVjdGAuIChlLmcuIGFycmF5cywgZnVuY3Rpb25zLCBvYmplY3RzLCByZWdleGVzLCBgbmV3IE51bWJlcigwKWAsIGFuZCBgbmV3IFN0cmluZygnJylgKVxuICpcbiAqIEBzdGF0aWNcbiAqIEBtZW1iZXJPZiBfXG4gKiBAc2luY2UgMC4xLjBcbiAqIEBjYXRlZ29yeSBMYW5nXG4gKiBAcGFyYW0geyp9IHZhbHVlIFRoZSB2YWx1ZSB0byBjaGVjay5cbiAqIEByZXR1cm5zIHtib29sZWFufSBSZXR1cm5zIGB0cnVlYCBpZiBgdmFsdWVgIGlzIGFuIG9iamVjdCwgZWxzZSBgZmFsc2VgLlxuICogQGV4YW1wbGVcbiAqXG4gKiBfLmlzT2JqZWN0KHt9KTtcbiAqIC8vID0+IHRydWVcbiAqXG4gKiBfLmlzT2JqZWN0KFsxLCAyLCAzXSk7XG4gKiAvLyA9PiB0cnVlXG4gKlxuICogXy5pc09iamVjdChfLm5vb3ApO1xuICogLy8gPT4gdHJ1ZVxuICpcbiAqIF8uaXNPYmplY3QobnVsbCk7XG4gKiAvLyA9PiBmYWxzZVxuICovXG5mdW5jdGlvbiBpc09iamVjdCh2YWx1ZSkge1xuICB2YXIgdHlwZSA9IHR5cGVvZiB2YWx1ZTtcbiAgcmV0dXJuICEhdmFsdWUgJiYgKHR5cGUgPT0gJ29iamVjdCcgfHwgdHlwZSA9PSAnZnVuY3Rpb24nKTtcbn1cblxuLyoqXG4gKiBDaGVja3MgaWYgYHZhbHVlYCBpcyBvYmplY3QtbGlrZS4gQSB2YWx1ZSBpcyBvYmplY3QtbGlrZSBpZiBpdCdzIG5vdCBgbnVsbGBcbiAqIGFuZCBoYXMgYSBgdHlwZW9mYCByZXN1bHQgb2YgXCJvYmplY3RcIi5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDQuMC4wXG4gKiBAY2F0ZWdvcnkgTGFuZ1xuICogQHBhcmFtIHsqfSB2YWx1ZSBUaGUgdmFsdWUgdG8gY2hlY2suXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gUmV0dXJucyBgdHJ1ZWAgaWYgYHZhbHVlYCBpcyBvYmplY3QtbGlrZSwgZWxzZSBgZmFsc2VgLlxuICogQGV4YW1wbGVcbiAqXG4gKiBfLmlzT2JqZWN0TGlrZSh7fSk7XG4gKiAvLyA9PiB0cnVlXG4gKlxuICogXy5pc09iamVjdExpa2UoWzEsIDIsIDNdKTtcbiAqIC8vID0+IHRydWVcbiAqXG4gKiBfLmlzT2JqZWN0TGlrZShfLm5vb3ApO1xuICogLy8gPT4gZmFsc2VcbiAqXG4gKiBfLmlzT2JqZWN0TGlrZShudWxsKTtcbiAqIC8vID0+IGZhbHNlXG4gKi9cbmZ1bmN0aW9uIGlzT2JqZWN0TGlrZSh2YWx1ZSkge1xuICByZXR1cm4gISF2YWx1ZSAmJiB0eXBlb2YgdmFsdWUgPT0gJ29iamVjdCc7XG59XG5cbi8qKlxuICogQ2hlY2tzIGlmIGB2YWx1ZWAgaXMgY2xhc3NpZmllZCBhcyBhIGBTeW1ib2xgIHByaW1pdGl2ZSBvciBvYmplY3QuXG4gKlxuICogQHN0YXRpY1xuICogQG1lbWJlck9mIF9cbiAqIEBzaW5jZSA0LjAuMFxuICogQGNhdGVnb3J5IExhbmdcbiAqIEBwYXJhbSB7Kn0gdmFsdWUgVGhlIHZhbHVlIHRvIGNoZWNrLlxuICogQHJldHVybnMge2Jvb2xlYW59IFJldHVybnMgYHRydWVgIGlmIGB2YWx1ZWAgaXMgYSBzeW1ib2wsIGVsc2UgYGZhbHNlYC5cbiAqIEBleGFtcGxlXG4gKlxuICogXy5pc1N5bWJvbChTeW1ib2wuaXRlcmF0b3IpO1xuICogLy8gPT4gdHJ1ZVxuICpcbiAqIF8uaXNTeW1ib2woJ2FiYycpO1xuICogLy8gPT4gZmFsc2VcbiAqL1xuZnVuY3Rpb24gaXNTeW1ib2wodmFsdWUpIHtcbiAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PSAnc3ltYm9sJyB8fFxuICAgIChpc09iamVjdExpa2UodmFsdWUpICYmIG9iamVjdFRvU3RyaW5nLmNhbGwodmFsdWUpID09IHN5bWJvbFRhZyk7XG59XG5cbi8qKlxuICogQ29udmVydHMgYHZhbHVlYCB0byBhIG51bWJlci5cbiAqXG4gKiBAc3RhdGljXG4gKiBAbWVtYmVyT2YgX1xuICogQHNpbmNlIDQuMC4wXG4gKiBAY2F0ZWdvcnkgTGFuZ1xuICogQHBhcmFtIHsqfSB2YWx1ZSBUaGUgdmFsdWUgdG8gcHJvY2Vzcy5cbiAqIEByZXR1cm5zIHtudW1iZXJ9IFJldHVybnMgdGhlIG51bWJlci5cbiAqIEBleGFtcGxlXG4gKlxuICogXy50b051bWJlcigzLjIpO1xuICogLy8gPT4gMy4yXG4gKlxuICogXy50b051bWJlcihOdW1iZXIuTUlOX1ZBTFVFKTtcbiAqIC8vID0+IDVlLTMyNFxuICpcbiAqIF8udG9OdW1iZXIoSW5maW5pdHkpO1xuICogLy8gPT4gSW5maW5pdHlcbiAqXG4gKiBfLnRvTnVtYmVyKCczLjInKTtcbiAqIC8vID0+IDMuMlxuICovXG5mdW5jdGlvbiB0b051bWJlcih2YWx1ZSkge1xuICBpZiAodHlwZW9mIHZhbHVlID09ICdudW1iZXInKSB7XG4gICAgcmV0dXJuIHZhbHVlO1xuICB9XG4gIGlmIChpc1N5bWJvbCh2YWx1ZSkpIHtcbiAgICByZXR1cm4gTkFOO1xuICB9XG4gIGlmIChpc09iamVjdCh2YWx1ZSkpIHtcbiAgICB2YXIgb3RoZXIgPSB0eXBlb2YgdmFsdWUudmFsdWVPZiA9PSAnZnVuY3Rpb24nID8gdmFsdWUudmFsdWVPZigpIDogdmFsdWU7XG4gICAgdmFsdWUgPSBpc09iamVjdChvdGhlcikgPyAob3RoZXIgKyAnJykgOiBvdGhlcjtcbiAgfVxuICBpZiAodHlwZW9mIHZhbHVlICE9ICdzdHJpbmcnKSB7XG4gICAgcmV0dXJuIHZhbHVlID09PSAwID8gdmFsdWUgOiArdmFsdWU7XG4gIH1cbiAgdmFsdWUgPSB2YWx1ZS5yZXBsYWNlKHJlVHJpbSwgJycpO1xuICB2YXIgaXNCaW5hcnkgPSByZUlzQmluYXJ5LnRlc3QodmFsdWUpO1xuICByZXR1cm4gKGlzQmluYXJ5IHx8IHJlSXNPY3RhbC50ZXN0KHZhbHVlKSlcbiAgICA/IGZyZWVQYXJzZUludCh2YWx1ZS5zbGljZSgyKSwgaXNCaW5hcnkgPyAyIDogOClcbiAgICA6IChyZUlzQmFkSGV4LnRlc3QodmFsdWUpID8gTkFOIDogK3ZhbHVlKTtcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB0aHJvdHRsZTtcbiIsInZhciBnO1xyXG5cclxuLy8gVGhpcyB3b3JrcyBpbiBub24tc3RyaWN0IG1vZGVcclxuZyA9IChmdW5jdGlvbigpIHtcclxuXHRyZXR1cm4gdGhpcztcclxufSkoKTtcclxuXHJcbnRyeSB7XHJcblx0Ly8gVGhpcyB3b3JrcyBpZiBldmFsIGlzIGFsbG93ZWQgKHNlZSBDU1ApXHJcblx0ZyA9IGcgfHwgRnVuY3Rpb24oXCJyZXR1cm4gdGhpc1wiKSgpIHx8ICgxLCBldmFsKShcInRoaXNcIik7XHJcbn0gY2F0Y2ggKGUpIHtcclxuXHQvLyBUaGlzIHdvcmtzIGlmIHRoZSB3aW5kb3cgcmVmZXJlbmNlIGlzIGF2YWlsYWJsZVxyXG5cdGlmICh0eXBlb2Ygd2luZG93ID09PSBcIm9iamVjdFwiKSBnID0gd2luZG93O1xyXG59XHJcblxyXG4vLyBnIGNhbiBzdGlsbCBiZSB1bmRlZmluZWQsIGJ1dCBub3RoaW5nIHRvIGRvIGFib3V0IGl0Li4uXHJcbi8vIFdlIHJldHVybiB1bmRlZmluZWQsIGluc3RlYWQgb2Ygbm90aGluZyBoZXJlLCBzbyBpdCdzXHJcbi8vIGVhc2llciB0byBoYW5kbGUgdGhpcyBjYXNlLiBpZighZ2xvYmFsKSB7IC4uLn1cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gZztcclxuIiwiLyotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbiAqICBDb3B5cmlnaHQgKGMpIE1pY3Jvc29mdCBDb3Jwb3JhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbiAqICBMaWNlbnNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuIFNlZSBMaWNlbnNlLnR4dCBpbiB0aGUgcHJvamVjdCByb290IGZvciBsaWNlbnNlIGluZm9ybWF0aW9uLlxuICotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSovXG5pbXBvcnQgeyBnZXRFbGVtZW50c0ZvclNvdXJjZUxpbmUgfSBmcm9tICcuL3Njcm9sbC1zeW5jJztcblxuZXhwb3J0IGNsYXNzIEFjdGl2ZUxpbmVNYXJrZXIge1xuXHRwcml2YXRlIF9jdXJyZW50OiBhbnk7XG5cblx0b25EaWRDaGFuZ2VUZXh0RWRpdG9yU2VsZWN0aW9uKGxpbmU6IG51bWJlcikge1xuXHRcdGNvbnN0IHsgcHJldmlvdXMgfSA9IGdldEVsZW1lbnRzRm9yU291cmNlTGluZShsaW5lKTtcblx0XHR0aGlzLl91cGRhdGUocHJldmlvdXMgJiYgcHJldmlvdXMuZWxlbWVudCk7XG5cdH1cblxuXHRfdXBkYXRlKGJlZm9yZTogSFRNTEVsZW1lbnQgfCB1bmRlZmluZWQpIHtcblx0XHR0aGlzLl91bm1hcmtBY3RpdmVFbGVtZW50KHRoaXMuX2N1cnJlbnQpO1xuXHRcdHRoaXMuX21hcmtBY3RpdmVFbGVtZW50KGJlZm9yZSk7XG5cdFx0dGhpcy5fY3VycmVudCA9IGJlZm9yZTtcblx0fVxuXG5cdF91bm1hcmtBY3RpdmVFbGVtZW50KGVsZW1lbnQ6IEhUTUxFbGVtZW50IHwgdW5kZWZpbmVkKSB7XG5cdFx0aWYgKCFlbGVtZW50KSB7XG5cdFx0XHRyZXR1cm47XG5cdFx0fVxuXHRcdGVsZW1lbnQuY2xhc3NOYW1lID0gZWxlbWVudC5jbGFzc05hbWUucmVwbGFjZSgvXFxiY29kZS1hY3RpdmUtbGluZVxcYi9nLCAnJyk7XG5cdH1cblxuXHRfbWFya0FjdGl2ZUVsZW1lbnQoZWxlbWVudDogSFRNTEVsZW1lbnQgfCB1bmRlZmluZWQpIHtcblx0XHRpZiAoIWVsZW1lbnQpIHtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cdFx0ZWxlbWVudC5jbGFzc05hbWUgKz0gJyBjb2RlLWFjdGl2ZS1saW5lJztcblx0fVxufSIsIi8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4gKiAgQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiAgTGljZW5zZWQgdW5kZXIgdGhlIE1JVCBMaWNlbnNlLiBTZWUgTGljZW5zZS50eHQgaW4gdGhlIHByb2plY3Qgcm9vdCBmb3IgbGljZW5zZSBpbmZvcm1hdGlvbi5cbiAqLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5leHBvcnQgZnVuY3Rpb24gb25jZURvY3VtZW50TG9hZGVkKGY6ICgpID0+IHZvaWQpIHtcblx0aWYgKGRvY3VtZW50LnJlYWR5U3RhdGUgPT09ICdsb2FkaW5nJyB8fCBkb2N1bWVudC5yZWFkeVN0YXRlIGFzIHN0cmluZyA9PT0gJ3VuaW5pdGlhbGl6ZWQnKSB7XG5cdFx0ZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignRE9NQ29udGVudExvYWRlZCcsIGYpO1xuXHR9IGVsc2Uge1xuXHRcdGYoKTtcblx0fVxufSIsIi8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4gKiAgQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiAgTGljZW5zZWQgdW5kZXIgdGhlIE1JVCBMaWNlbnNlLiBTZWUgTGljZW5zZS50eHQgaW4gdGhlIHByb2plY3Qgcm9vdCBmb3IgbGljZW5zZSBpbmZvcm1hdGlvbi5cbiAqLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5pbXBvcnQgeyBBY3RpdmVMaW5lTWFya2VyIH0gZnJvbSAnLi9hY3RpdmVMaW5lTWFya2VyJztcbmltcG9ydCB7IG9uY2VEb2N1bWVudExvYWRlZCB9IGZyb20gJy4vZXZlbnRzJztcbmltcG9ydCB7IGNyZWF0ZVBvc3RlckZvclZzQ29kZSB9IGZyb20gJy4vbWVzc2FnaW5nJztcbmltcG9ydCB7IGdldEVkaXRvckxpbmVOdW1iZXJGb3JQYWdlT2Zmc2V0LCBzY3JvbGxUb1JldmVhbFNvdXJjZUxpbmUsIGdldExpbmVFbGVtZW50Rm9yRnJhZ21lbnQgfSBmcm9tICcuL3Njcm9sbC1zeW5jJztcbmltcG9ydCB7IGdldFNldHRpbmdzLCBnZXREYXRhIH0gZnJvbSAnLi9zZXR0aW5ncyc7XG5pbXBvcnQgdGhyb3R0bGUgPSByZXF1aXJlKCdsb2Rhc2gudGhyb3R0bGUnKTtcblxuZGVjbGFyZSB2YXIgYWNxdWlyZVZzQ29kZUFwaTogYW55O1xuXG5sZXQgc2Nyb2xsRGlzYWJsZWQgPSB0cnVlO1xuY29uc3QgbWFya2VyID0gbmV3IEFjdGl2ZUxpbmVNYXJrZXIoKTtcbmNvbnN0IHNldHRpbmdzID0gZ2V0U2V0dGluZ3MoKTtcblxuY29uc3QgdnNjb2RlID0gYWNxdWlyZVZzQ29kZUFwaSgpO1xuXG4vLyBTZXQgVlMgQ29kZSBzdGF0ZVxubGV0IHN0YXRlID0gZ2V0RGF0YTx7IGxpbmU6IG51bWJlciwgIGZyYWdtZW50OiBzdHJpbmcgfT4oJ2RhdGEtc3RhdGUnKTtcbnZzY29kZS5zZXRTdGF0ZShzdGF0ZSk7XG5cbmNvbnN0IG1lc3NhZ2luZyA9IGNyZWF0ZVBvc3RlckZvclZzQ29kZSh2c2NvZGUpO1xuXG53aW5kb3cuY3NwQWxlcnRlci5zZXRQb3N0ZXIobWVzc2FnaW5nKTtcbndpbmRvdy5zdHlsZUxvYWRpbmdNb25pdG9yLnNldFBvc3RlcihtZXNzYWdpbmcpO1xuXG53aW5kb3cub25sb2FkID0gKCkgPT4ge1xuXHR1cGRhdGVJbWFnZVNpemVzKCk7XG59O1xuXG5vbmNlRG9jdW1lbnRMb2FkZWQoKCkgPT4ge1xuXHRpZiAoc2V0dGluZ3Muc2Nyb2xsUHJldmlld1dpdGhFZGl0b3IpIHtcblx0XHRzZXRUaW1lb3V0KCgpID0+IHtcblx0XHRcdC8vIFRyeSB0byBzY3JvbGwgdG8gZnJhZ21lbnQgaWYgYXZhaWxhYmxlXG5cdFx0XHRpZiAoc3RhdGUuZnJhZ21lbnQpIHtcblx0XHRcdFx0Y29uc3QgZWxlbWVudCA9IGdldExpbmVFbGVtZW50Rm9yRnJhZ21lbnQoc3RhdGUuZnJhZ21lbnQpO1xuXHRcdFx0XHRpZiAoZWxlbWVudCkge1xuXHRcdFx0XHRcdHNjcm9sbERpc2FibGVkID0gdHJ1ZTtcblx0XHRcdFx0XHRzY3JvbGxUb1JldmVhbFNvdXJjZUxpbmUoZWxlbWVudC5saW5lKTtcblx0XHRcdFx0fVxuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0Y29uc3QgaW5pdGlhbExpbmUgPSArc2V0dGluZ3MubGluZTtcblx0XHRcdFx0aWYgKCFpc05hTihpbml0aWFsTGluZSkpIHtcblx0XHRcdFx0XHRzY3JvbGxEaXNhYmxlZCA9IHRydWU7XG5cdFx0XHRcdFx0c2Nyb2xsVG9SZXZlYWxTb3VyY2VMaW5lKGluaXRpYWxMaW5lKTtcblx0XHRcdFx0fVxuXHRcdFx0fVxuXHRcdH0sIDApO1xuXHR9XG59KTtcblxuY29uc3Qgb25VcGRhdGVWaWV3ID0gKCgpID0+IHtcblx0Y29uc3QgZG9TY3JvbGwgPSB0aHJvdHRsZSgobGluZTogbnVtYmVyKSA9PiB7XG5cdFx0c2Nyb2xsRGlzYWJsZWQgPSB0cnVlO1xuXHRcdHNjcm9sbFRvUmV2ZWFsU291cmNlTGluZShsaW5lKTtcblx0fSwgNTApO1xuXG5cdHJldHVybiAobGluZTogbnVtYmVyLCBzZXR0aW5nczogYW55KSA9PiB7XG5cdFx0aWYgKCFpc05hTihsaW5lKSkge1xuXHRcdFx0c2V0dGluZ3MubGluZSA9IGxpbmU7XG5cdFx0XHRkb1Njcm9sbChsaW5lKTtcblx0XHR9XG5cdH07XG59KSgpO1xuXG5sZXQgdXBkYXRlSW1hZ2VTaXplcyA9IHRocm90dGxlKCgpID0+IHtcblx0Y29uc3QgaW1hZ2VJbmZvOiB7IGlkOiBzdHJpbmcsIGhlaWdodDogbnVtYmVyLCB3aWR0aDogbnVtYmVyIH1bXSA9IFtdO1xuXHRsZXQgaW1hZ2VzID0gZG9jdW1lbnQuZ2V0RWxlbWVudHNCeVRhZ05hbWUoJ2ltZycpO1xuXHRpZiAoaW1hZ2VzKSB7XG5cdFx0bGV0IGk7XG5cdFx0Zm9yIChpID0gMDsgaSA8IGltYWdlcy5sZW5ndGg7IGkrKykge1xuXHRcdFx0Y29uc3QgaW1nID0gaW1hZ2VzW2ldO1xuXG5cdFx0XHRpZiAoaW1nLmNsYXNzTGlzdC5jb250YWlucygnbG9hZGluZycpKSB7XG5cdFx0XHRcdGltZy5jbGFzc0xpc3QucmVtb3ZlKCdsb2FkaW5nJyk7XG5cdFx0XHR9XG5cblx0XHRcdGltYWdlSW5mby5wdXNoKHtcblx0XHRcdFx0aWQ6IGltZy5pZCxcblx0XHRcdFx0aGVpZ2h0OiBpbWcuaGVpZ2h0LFxuXHRcdFx0XHR3aWR0aDogaW1nLndpZHRoXG5cdFx0XHR9KTtcblx0XHR9XG5cblx0XHRtZXNzYWdpbmcucG9zdE1lc3NhZ2UoJ2NhY2hlSW1hZ2VTaXplcycsIGltYWdlSW5mbyk7XG5cdH1cbn0sIDUwKTtcblxud2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ3Jlc2l6ZScsICgpID0+IHtcblx0c2Nyb2xsRGlzYWJsZWQgPSB0cnVlO1xuXHR1cGRhdGVJbWFnZVNpemVzKCk7XG59LCB0cnVlKTtcblxud2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ21lc3NhZ2UnLCBldmVudCA9PiB7XG5cdGlmIChldmVudC5kYXRhLnNvdXJjZSAhPT0gc2V0dGluZ3Muc291cmNlKSB7XG5cdFx0cmV0dXJuO1xuXHR9XG5cblx0c3dpdGNoIChldmVudC5kYXRhLnR5cGUpIHtcblx0XHRjYXNlICdvbkRpZENoYW5nZVRleHRFZGl0b3JTZWxlY3Rpb24nOlxuXHRcdFx0bWFya2VyLm9uRGlkQ2hhbmdlVGV4dEVkaXRvclNlbGVjdGlvbihldmVudC5kYXRhLmxpbmUpO1xuXHRcdFx0YnJlYWs7XG5cblx0XHRjYXNlICd1cGRhdGVWaWV3Jzpcblx0XHRcdG9uVXBkYXRlVmlldyhldmVudC5kYXRhLmxpbmUsIHNldHRpbmdzKTtcblx0XHRcdGJyZWFrO1xuXHR9XG59LCBmYWxzZSk7XG5cbmRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2RibGNsaWNrJywgZXZlbnQgPT4ge1xuXHRpZiAoIXNldHRpbmdzLmRvdWJsZUNsaWNrVG9Td2l0Y2hUb0VkaXRvcikge1xuXHRcdHJldHVybjtcblx0fVxuXG5cdC8vIElnbm9yZSBjbGlja3Mgb24gbGlua3Ncblx0Zm9yIChsZXQgbm9kZSA9IGV2ZW50LnRhcmdldCBhcyBIVE1MRWxlbWVudDsgbm9kZTsgbm9kZSA9IG5vZGUucGFyZW50Tm9kZSBhcyBIVE1MRWxlbWVudCkge1xuXHRcdGlmIChub2RlLnRhZ05hbWUgPT09ICdBJykge1xuXHRcdFx0cmV0dXJuO1xuXHRcdH1cblx0fVxuXG5cdGNvbnN0IG9mZnNldCA9IGV2ZW50LnBhZ2VZO1xuXHRjb25zdCBsaW5lID0gZ2V0RWRpdG9yTGluZU51bWJlckZvclBhZ2VPZmZzZXQob2Zmc2V0KTtcblx0aWYgKHR5cGVvZiBsaW5lID09PSAnbnVtYmVyJyAmJiAhaXNOYU4obGluZSkpIHtcblx0XHRtZXNzYWdpbmcucG9zdE1lc3NhZ2UoJ2RpZENsaWNrJywgeyBsaW5lOiBNYXRoLmZsb29yKGxpbmUpIH0pO1xuXHR9XG59KTtcblxuZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCBldmVudCA9PiB7XG5cdGlmICghZXZlbnQpIHtcblx0XHRyZXR1cm47XG5cdH1cblxuXHRsZXQgbm9kZTogYW55ID0gZXZlbnQudGFyZ2V0O1xuXHR3aGlsZSAobm9kZSkge1xuXHRcdGlmIChub2RlLnRhZ05hbWUgJiYgbm9kZS50YWdOYW1lID09PSAnQScgJiYgbm9kZS5ocmVmKSB7XG5cdFx0XHRpZiAobm9kZS5nZXRBdHRyaWJ1dGUoJ2hyZWYnKS5zdGFydHNXaXRoKCcjJykpIHtcblx0XHRcdFx0YnJlYWs7XG5cdFx0XHR9XG5cdFx0XHRpZiAobm9kZS5ocmVmLnN0YXJ0c1dpdGgoJ2ZpbGU6Ly8nKSB8fCBub2RlLmhyZWYuc3RhcnRzV2l0aCgndnNjb2RlLXJlc291cmNlOicpIHx8IG5vZGUuaHJlZi5zdGFydHNXaXRoKHNldHRpbmdzLndlYnZpZXdSZXNvdXJjZVJvb3QpKSB7XG5cdFx0XHRcdGNvbnN0IFtwYXRoLCBmcmFnbWVudF0gPSBub2RlLmhyZWYucmVwbGFjZSgvXihmaWxlOlxcL1xcL3x2c2NvZGUtcmVzb3VyY2U6KS9pLCAnJykucmVwbGFjZShuZXcgUmVnRXhwKGBeJHtlc2NhcGVSZWdFeHAoc2V0dGluZ3Mud2Vidmlld1Jlc291cmNlUm9vdCl9YCkpLnNwbGl0KCcjJyk7XG5cdFx0XHRcdG1lc3NhZ2luZy5wb3N0TWVzc2FnZSgnY2xpY2tMaW5rJywgeyBwYXRoLCBmcmFnbWVudCB9KTtcblx0XHRcdFx0ZXZlbnQucHJldmVudERlZmF1bHQoKTtcblx0XHRcdFx0ZXZlbnQuc3RvcFByb3BhZ2F0aW9uKCk7XG5cdFx0XHRcdGJyZWFrO1xuXHRcdFx0fVxuXHRcdFx0YnJlYWs7XG5cdFx0fVxuXHRcdG5vZGUgPSBub2RlLnBhcmVudE5vZGU7XG5cdH1cbn0sIHRydWUpO1xuXG5pZiAoc2V0dGluZ3Muc2Nyb2xsRWRpdG9yV2l0aFByZXZpZXcpIHtcblx0d2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ3Njcm9sbCcsIHRocm90dGxlKCgpID0+IHtcblx0XHRpZiAoc2Nyb2xsRGlzYWJsZWQpIHtcblx0XHRcdHNjcm9sbERpc2FibGVkID0gZmFsc2U7XG5cdFx0fSBlbHNlIHtcblx0XHRcdGNvbnN0IGxpbmUgPSBnZXRFZGl0b3JMaW5lTnVtYmVyRm9yUGFnZU9mZnNldCh3aW5kb3cuc2Nyb2xsWSk7XG5cdFx0XHRpZiAodHlwZW9mIGxpbmUgPT09ICdudW1iZXInICYmICFpc05hTihsaW5lKSkge1xuXHRcdFx0XHRtZXNzYWdpbmcucG9zdE1lc3NhZ2UoJ3JldmVhbExpbmUnLCB7IGxpbmUgfSk7XG5cdFx0XHRcdHN0YXRlLmxpbmUgPSBsaW5lO1xuXHRcdFx0XHR2c2NvZGUuc2V0U3RhdGUoc3RhdGUpO1xuXHRcdFx0fVxuXHRcdH1cblx0fSwgNTApKTtcbn1cblxuZnVuY3Rpb24gZXNjYXBlUmVnRXhwKHRleHQ6IHN0cmluZykge1xuXHRyZXR1cm4gdGV4dC5yZXBsYWNlKC9bLVtcXF17fSgpKis/LixcXFxcXiR8I1xcc10vZywgJ1xcXFwkJicpO1xufVxuIiwiLyotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbiAqICBDb3B5cmlnaHQgKGMpIE1pY3Jvc29mdCBDb3Jwb3JhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbiAqICBMaWNlbnNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuIFNlZSBMaWNlbnNlLnR4dCBpbiB0aGUgcHJvamVjdCByb290IGZvciBsaWNlbnNlIGluZm9ybWF0aW9uLlxuICotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSovXG5cbmltcG9ydCB7IGdldFNldHRpbmdzIH0gZnJvbSAnLi9zZXR0aW5ncyc7XG5cbmV4cG9ydCBpbnRlcmZhY2UgTWVzc2FnZVBvc3RlciB7XG5cdC8qKlxuXHQgKiBQb3N0IGEgbWVzc2FnZSB0byB0aGUgbWFya2Rvd24gZXh0ZW5zaW9uXG5cdCAqL1xuXHRwb3N0TWVzc2FnZSh0eXBlOiBzdHJpbmcsIGJvZHk6IG9iamVjdCk6IHZvaWQ7XG59XG5cbmV4cG9ydCBjb25zdCBjcmVhdGVQb3N0ZXJGb3JWc0NvZGUgPSAodnNjb2RlOiBhbnkpID0+IHtcblx0cmV0dXJuIG5ldyBjbGFzcyBpbXBsZW1lbnRzIE1lc3NhZ2VQb3N0ZXIge1xuXHRcdHBvc3RNZXNzYWdlKHR5cGU6IHN0cmluZywgYm9keTogb2JqZWN0KTogdm9pZCB7XG5cdFx0XHR2c2NvZGUucG9zdE1lc3NhZ2Uoe1xuXHRcdFx0XHR0eXBlLFxuXHRcdFx0XHRzb3VyY2U6IGdldFNldHRpbmdzKCkuc291cmNlLFxuXHRcdFx0XHRib2R5XG5cdFx0XHR9KTtcblx0XHR9XG5cdH07XG59O1xuXG4iLCIvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuICogIENvcHlyaWdodCAoYykgTWljcm9zb2Z0IENvcnBvcmF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuICogIExpY2Vuc2VkIHVuZGVyIHRoZSBNSVQgTGljZW5zZS4gU2VlIExpY2Vuc2UudHh0IGluIHRoZSBwcm9qZWN0IHJvb3QgZm9yIGxpY2Vuc2UgaW5mb3JtYXRpb24uXG4gKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuaW1wb3J0IHsgZ2V0U2V0dGluZ3MgfSBmcm9tICcuL3NldHRpbmdzJztcblxuXG5mdW5jdGlvbiBjbGFtcChtaW46IG51bWJlciwgbWF4OiBudW1iZXIsIHZhbHVlOiBudW1iZXIpIHtcblx0cmV0dXJuIE1hdGgubWluKG1heCwgTWF0aC5tYXgobWluLCB2YWx1ZSkpO1xufVxuXG5mdW5jdGlvbiBjbGFtcExpbmUobGluZTogbnVtYmVyKSB7XG5cdHJldHVybiBjbGFtcCgwLCBnZXRTZXR0aW5ncygpLmxpbmVDb3VudCAtIDEsIGxpbmUpO1xufVxuXG5cbmV4cG9ydCBpbnRlcmZhY2UgQ29kZUxpbmVFbGVtZW50IHtcblx0ZWxlbWVudDogSFRNTEVsZW1lbnQ7XG5cdGxpbmU6IG51bWJlcjtcbn1cblxuY29uc3QgZ2V0Q29kZUxpbmVFbGVtZW50cyA9ICgoKSA9PiB7XG5cdGxldCBlbGVtZW50czogQ29kZUxpbmVFbGVtZW50W107XG5cdHJldHVybiAoKSA9PiB7XG5cdFx0aWYgKCFlbGVtZW50cykge1xuXHRcdFx0ZWxlbWVudHMgPSBbeyBlbGVtZW50OiBkb2N1bWVudC5ib2R5LCBsaW5lOiAwIH1dO1xuXHRcdFx0Zm9yIChjb25zdCBlbGVtZW50IG9mIGRvY3VtZW50LmdldEVsZW1lbnRzQnlDbGFzc05hbWUoJ2NvZGUtbGluZScpKSB7XG5cdFx0XHRcdGNvbnN0IGxpbmUgPSArZWxlbWVudC5nZXRBdHRyaWJ1dGUoJ2RhdGEtbGluZScpITtcblx0XHRcdFx0aWYgKCFpc05hTihsaW5lKSkge1xuXHRcdFx0XHRcdGVsZW1lbnRzLnB1c2goeyBlbGVtZW50OiBlbGVtZW50IGFzIEhUTUxFbGVtZW50LCBsaW5lIH0pO1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiBlbGVtZW50cztcblx0fTtcbn0pKCk7XG5cbi8qKlxuICogRmluZCB0aGUgaHRtbCBlbGVtZW50cyB0aGF0IG1hcCB0byBhIHNwZWNpZmljIHRhcmdldCBsaW5lIGluIHRoZSBlZGl0b3IuXG4gKlxuICogSWYgYW4gZXhhY3QgbWF0Y2gsIHJldHVybnMgYSBzaW5nbGUgZWxlbWVudC4gSWYgdGhlIGxpbmUgaXMgYmV0d2VlbiBlbGVtZW50cyxcbiAqIHJldHVybnMgdGhlIGVsZW1lbnQgcHJpb3IgdG8gYW5kIHRoZSBlbGVtZW50IGFmdGVyIHRoZSBnaXZlbiBsaW5lLlxuICovXG5leHBvcnQgZnVuY3Rpb24gZ2V0RWxlbWVudHNGb3JTb3VyY2VMaW5lKHRhcmdldExpbmU6IG51bWJlcik6IHsgcHJldmlvdXM6IENvZGVMaW5lRWxlbWVudDsgbmV4dD86IENvZGVMaW5lRWxlbWVudDsgfSB7XG5cdGNvbnN0IGxpbmVOdW1iZXIgPSBNYXRoLmZsb29yKHRhcmdldExpbmUpO1xuXHRjb25zdCBsaW5lcyA9IGdldENvZGVMaW5lRWxlbWVudHMoKTtcblx0bGV0IHByZXZpb3VzID0gbGluZXNbMF0gfHwgbnVsbDtcblx0Zm9yIChjb25zdCBlbnRyeSBvZiBsaW5lcykge1xuXHRcdGlmIChlbnRyeS5saW5lID09PSBsaW5lTnVtYmVyKSB7XG5cdFx0XHRyZXR1cm4geyBwcmV2aW91czogZW50cnksIG5leHQ6IHVuZGVmaW5lZCB9O1xuXHRcdH0gZWxzZSBpZiAoZW50cnkubGluZSA+IGxpbmVOdW1iZXIpIHtcblx0XHRcdHJldHVybiB7IHByZXZpb3VzLCBuZXh0OiBlbnRyeSB9O1xuXHRcdH1cblx0XHRwcmV2aW91cyA9IGVudHJ5O1xuXHR9XG5cdHJldHVybiB7IHByZXZpb3VzIH07XG59XG5cbi8qKlxuICogRmluZCB0aGUgaHRtbCBlbGVtZW50cyB0aGF0IGFyZSBhdCBhIHNwZWNpZmljIHBpeGVsIG9mZnNldCBvbiB0aGUgcGFnZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdldExpbmVFbGVtZW50c0F0UGFnZU9mZnNldChvZmZzZXQ6IG51bWJlcik6IHsgcHJldmlvdXM6IENvZGVMaW5lRWxlbWVudDsgbmV4dD86IENvZGVMaW5lRWxlbWVudDsgfSB7XG5cdGNvbnN0IGxpbmVzID0gZ2V0Q29kZUxpbmVFbGVtZW50cygpO1xuXHRjb25zdCBwb3NpdGlvbiA9IG9mZnNldCAtIHdpbmRvdy5zY3JvbGxZO1xuXHRsZXQgbG8gPSAtMTtcblx0bGV0IGhpID0gbGluZXMubGVuZ3RoIC0gMTtcblx0d2hpbGUgKGxvICsgMSA8IGhpKSB7XG5cdFx0Y29uc3QgbWlkID0gTWF0aC5mbG9vcigobG8gKyBoaSkgLyAyKTtcblx0XHRjb25zdCBib3VuZHMgPSBsaW5lc1ttaWRdLmVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG5cdFx0aWYgKGJvdW5kcy50b3AgKyBib3VuZHMuaGVpZ2h0ID49IHBvc2l0aW9uKSB7XG5cdFx0XHRoaSA9IG1pZDtcblx0XHR9XG5cdFx0ZWxzZSB7XG5cdFx0XHRsbyA9IG1pZDtcblx0XHR9XG5cdH1cblx0Y29uc3QgaGlFbGVtZW50ID0gbGluZXNbaGldO1xuXHRjb25zdCBoaUJvdW5kcyA9IGhpRWxlbWVudC5lbGVtZW50LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuXHRpZiAoaGkgPj0gMSAmJiBoaUJvdW5kcy50b3AgPiBwb3NpdGlvbikge1xuXHRcdGNvbnN0IGxvRWxlbWVudCA9IGxpbmVzW2xvXTtcblx0XHRyZXR1cm4geyBwcmV2aW91czogbG9FbGVtZW50LCBuZXh0OiBoaUVsZW1lbnQgfTtcblx0fVxuXHRyZXR1cm4geyBwcmV2aW91czogaGlFbGVtZW50IH07XG59XG5cbi8qKlxuICogQXR0ZW1wdCB0byByZXZlYWwgdGhlIGVsZW1lbnQgZm9yIGEgc291cmNlIGxpbmUgaW4gdGhlIGVkaXRvci5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHNjcm9sbFRvUmV2ZWFsU291cmNlTGluZShsaW5lOiBudW1iZXIpIHtcblx0aWYgKCFnZXRTZXR0aW5ncygpLnNjcm9sbFByZXZpZXdXaXRoRWRpdG9yKSB7XG5cdFx0cmV0dXJuO1xuXHR9XG5cblx0aWYgKGxpbmUgPD0gMCkge1xuXHRcdHdpbmRvdy5zY3JvbGwod2luZG93LnNjcm9sbFgsIDApO1xuXHRcdHJldHVybjtcblx0fVxuXG5cdGNvbnN0IHsgcHJldmlvdXMsIG5leHQgfSA9IGdldEVsZW1lbnRzRm9yU291cmNlTGluZShsaW5lKTtcblx0aWYgKCFwcmV2aW91cykge1xuXHRcdHJldHVybjtcblx0fVxuXHRsZXQgc2Nyb2xsVG8gPSAwO1xuXHRjb25zdCByZWN0ID0gcHJldmlvdXMuZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcblx0Y29uc3QgcHJldmlvdXNUb3AgPSByZWN0LnRvcDtcblx0aWYgKG5leHQgJiYgbmV4dC5saW5lICE9PSBwcmV2aW91cy5saW5lKSB7XG5cdFx0Ly8gQmV0d2VlbiB0d28gZWxlbWVudHMuIEdvIHRvIHBlcmNlbnRhZ2Ugb2Zmc2V0IGJldHdlZW4gdGhlbS5cblx0XHRjb25zdCBiZXR3ZWVuUHJvZ3Jlc3MgPSAobGluZSAtIHByZXZpb3VzLmxpbmUpIC8gKG5leHQubGluZSAtIHByZXZpb3VzLmxpbmUpO1xuXHRcdGNvbnN0IGVsZW1lbnRPZmZzZXQgPSBuZXh0LmVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCkudG9wIC0gcHJldmlvdXNUb3A7XG5cdFx0c2Nyb2xsVG8gPSBwcmV2aW91c1RvcCArIGJldHdlZW5Qcm9ncmVzcyAqIGVsZW1lbnRPZmZzZXQ7XG5cdH0gZWxzZSB7XG5cdFx0Y29uc3QgcHJvZ3Jlc3NJbkVsZW1lbnQgPSBsaW5lIC0gTWF0aC5mbG9vcihsaW5lKTtcblx0XHRzY3JvbGxUbyA9IHByZXZpb3VzVG9wICsgKHJlY3QuaGVpZ2h0ICogcHJvZ3Jlc3NJbkVsZW1lbnQpO1xuXHR9XG5cdHdpbmRvdy5zY3JvbGwod2luZG93LnNjcm9sbFgsIE1hdGgubWF4KDEsIHdpbmRvdy5zY3JvbGxZICsgc2Nyb2xsVG8pKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGdldEVkaXRvckxpbmVOdW1iZXJGb3JQYWdlT2Zmc2V0KG9mZnNldDogbnVtYmVyKSB7XG5cdGNvbnN0IHsgcHJldmlvdXMsIG5leHQgfSA9IGdldExpbmVFbGVtZW50c0F0UGFnZU9mZnNldChvZmZzZXQpO1xuXHRpZiAocHJldmlvdXMpIHtcblx0XHRjb25zdCBwcmV2aW91c0JvdW5kcyA9IHByZXZpb3VzLmVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG5cdFx0Y29uc3Qgb2Zmc2V0RnJvbVByZXZpb3VzID0gKG9mZnNldCAtIHdpbmRvdy5zY3JvbGxZIC0gcHJldmlvdXNCb3VuZHMudG9wKTtcblx0XHRpZiAobmV4dCkge1xuXHRcdFx0Y29uc3QgcHJvZ3Jlc3NCZXR3ZWVuRWxlbWVudHMgPSBvZmZzZXRGcm9tUHJldmlvdXMgLyAobmV4dC5lbGVtZW50LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpLnRvcCAtIHByZXZpb3VzQm91bmRzLnRvcCk7XG5cdFx0XHRjb25zdCBsaW5lID0gcHJldmlvdXMubGluZSArIHByb2dyZXNzQmV0d2VlbkVsZW1lbnRzICogKG5leHQubGluZSAtIHByZXZpb3VzLmxpbmUpO1xuXHRcdFx0cmV0dXJuIGNsYW1wTGluZShsaW5lKTtcblx0XHR9XG5cdFx0ZWxzZSB7XG5cdFx0XHRjb25zdCBwcm9ncmVzc1dpdGhpbkVsZW1lbnQgPSBvZmZzZXRGcm9tUHJldmlvdXMgLyAocHJldmlvdXNCb3VuZHMuaGVpZ2h0KTtcblx0XHRcdGNvbnN0IGxpbmUgPSBwcmV2aW91cy5saW5lICsgcHJvZ3Jlc3NXaXRoaW5FbGVtZW50O1xuXHRcdFx0cmV0dXJuIGNsYW1wTGluZShsaW5lKTtcblx0XHR9XG5cdH1cblx0cmV0dXJuIG51bGw7XG59XG5cbi8qKlxuICogVHJ5IHRvIGZpbmQgdGhlIGh0bWwgZWxlbWVudCBieSB1c2luZyBhIGZyYWdtZW50IGlkXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZXRMaW5lRWxlbWVudEZvckZyYWdtZW50KGZyYWdtZW50OiBzdHJpbmcpOiBDb2RlTGluZUVsZW1lbnQgfCB1bmRlZmluZWQge1xuXHRyZXR1cm4gZ2V0Q29kZUxpbmVFbGVtZW50cygpLmZpbmQoKGVsZW1lbnQpID0+IHtcblx0XHRyZXR1cm4gZWxlbWVudC5lbGVtZW50LmlkID09PSBmcmFnbWVudDtcblx0fSk7XG59XG4iLCIvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuICogIENvcHlyaWdodCAoYykgTWljcm9zb2Z0IENvcnBvcmF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuICogIExpY2Vuc2VkIHVuZGVyIHRoZSBNSVQgTGljZW5zZS4gU2VlIExpY2Vuc2UudHh0IGluIHRoZSBwcm9qZWN0IHJvb3QgZm9yIGxpY2Vuc2UgaW5mb3JtYXRpb24uXG4gKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuZXhwb3J0IGludGVyZmFjZSBQcmV2aWV3U2V0dGluZ3Mge1xuXHRyZWFkb25seSBzb3VyY2U6IHN0cmluZztcblx0cmVhZG9ubHkgbGluZTogbnVtYmVyO1xuXHRyZWFkb25seSBsaW5lQ291bnQ6IG51bWJlcjtcblx0cmVhZG9ubHkgc2Nyb2xsUHJldmlld1dpdGhFZGl0b3I/OiBib29sZWFuO1xuXHRyZWFkb25seSBzY3JvbGxFZGl0b3JXaXRoUHJldmlldzogYm9vbGVhbjtcblx0cmVhZG9ubHkgZGlzYWJsZVNlY3VyaXR5V2FybmluZ3M6IGJvb2xlYW47XG5cdHJlYWRvbmx5IGRvdWJsZUNsaWNrVG9Td2l0Y2hUb0VkaXRvcjogYm9vbGVhbjtcblx0cmVhZG9ubHkgd2Vidmlld1Jlc291cmNlUm9vdDogc3RyaW5nO1xufVxuXG5sZXQgY2FjaGVkU2V0dGluZ3M6IFByZXZpZXdTZXR0aW5ncyB8IHVuZGVmaW5lZCA9IHVuZGVmaW5lZDtcblxuZXhwb3J0IGZ1bmN0aW9uIGdldERhdGE8VCA9IHt9PihrZXk6IHN0cmluZyk6IFQge1xuXHRjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3ZzY29kZS1tYXJrZG93bi1wcmV2aWV3LWRhdGEnKTtcblx0aWYgKGVsZW1lbnQpIHtcblx0XHRjb25zdCBkYXRhID0gZWxlbWVudC5nZXRBdHRyaWJ1dGUoa2V5KTtcblx0XHRpZiAoZGF0YSkge1xuXHRcdFx0cmV0dXJuIEpTT04ucGFyc2UoZGF0YSk7XG5cdFx0fVxuXHR9XG5cblx0dGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgbG9hZCBkYXRhIGZvciAke2tleX1gKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGdldFNldHRpbmdzKCk6IFByZXZpZXdTZXR0aW5ncyB7XG5cdGlmIChjYWNoZWRTZXR0aW5ncykge1xuXHRcdHJldHVybiBjYWNoZWRTZXR0aW5ncztcblx0fVxuXG5cdGNhY2hlZFNldHRpbmdzID0gZ2V0RGF0YSgnZGF0YS1zZXR0aW5ncycpO1xuXHRpZiAoY2FjaGVkU2V0dGluZ3MpIHtcblx0XHRyZXR1cm4gY2FjaGVkU2V0dGluZ3M7XG5cdH1cblxuXHR0aHJvdyBuZXcgRXJyb3IoJ0NvdWxkIG5vdCBsb2FkIHNldHRpbmdzJyk7XG59XG4iXSwic291cmNlUm9vdCI6IiJ9 \ No newline at end of file diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 0d74e1ac674c6..19bd9ef8b1a66 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -24,6 +24,7 @@ "onCommand:markdown.showLockedPreviewToSide", "onCommand:markdown.showSource", "onCommand:markdown.showPreviewSecuritySelector", + "onCommand:markdown.api.render", "onWebviewPanel:markdown.preview" ], "contributes": { @@ -309,7 +310,7 @@ }, "dependencies": { "highlight.js": "9.15.8", - "markdown-it": "^8.4.2", + "markdown-it": "^9.1.0", "markdown-it-front-matter": "^0.1.2", "vscode-extension-telemetry": "0.1.1", "vscode-nls": "^4.0.0" @@ -328,4 +329,4 @@ "webpack": "^4.1.0", "webpack-cli": "^2.0.10" } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index f78ae2f79724c..b12614fe9ec58 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -6,7 +6,7 @@ import { ActiveLineMarker } from './activeLineMarker'; import { onceDocumentLoaded } from './events'; import { createPosterForVsCode } from './messaging'; -import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine } from './scroll-sync'; +import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElementForFragment } from './scroll-sync'; import { getSettings, getData } from './settings'; import throttle = require('lodash.throttle'); @@ -19,7 +19,7 @@ const settings = getSettings(); const vscode = acquireVsCodeApi(); // Set VS Code state -let state = getData<{ line: number }>('data-state'); +let state = getData<{ line: number, fragment: string }>('data-state'); vscode.setState(state); const messaging = createPosterForVsCode(vscode); @@ -34,10 +34,19 @@ window.onload = () => { onceDocumentLoaded(() => { if (settings.scrollPreviewWithEditor) { setTimeout(() => { - const initialLine = +settings.line; - if (!isNaN(initialLine)) { - scrollDisabled = true; - scrollToRevealSourceLine(initialLine); + // Try to scroll to fragment if available + if (state.fragment) { + const element = getLineElementForFragment(state.fragment); + if (element) { + scrollDisabled = true; + scrollToRevealSourceLine(element.line); + } + } else { + const initialLine = +settings.line; + if (!isNaN(initialLine)) { + scrollDisabled = true; + scrollToRevealSourceLine(initialLine); + } } }, 0); } @@ -161,4 +170,4 @@ if (settings.scrollEditorWithPreview) { function escapeRegExp(text: string) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/preview-src/scroll-sync.ts b/extensions/markdown-language-features/preview-src/scroll-sync.ts index 4fe8987f6cddc..eee320ac8e7f6 100644 --- a/extensions/markdown-language-features/preview-src/scroll-sync.ts +++ b/extensions/markdown-language-features/preview-src/scroll-sync.ts @@ -134,3 +134,12 @@ export function getEditorLineNumberForPageOffset(offset: number) { } return null; } + +/** + * Try to find the html element by using a fragment id + */ +export function getLineElementForFragment(fragment: string): CodeLineElement | undefined { + return getCodeLineElements().find((element) => { + return element.element.id === fragment; + }); +} diff --git a/extensions/markdown-language-features/src/commands/index.ts b/extensions/markdown-language-features/src/commands/index.ts index e8c9651ee0c76..68aff7ffcf5be 100644 --- a/extensions/markdown-language-features/src/commands/index.ts +++ b/extensions/markdown-language-features/src/commands/index.ts @@ -10,3 +10,4 @@ export { RefreshPreviewCommand } from './refreshPreview'; export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector'; export { MoveCursorToPositionCommand } from './moveCursorToPosition'; export { ToggleLockCommand } from './toggleLock'; +export { RenderDocument } from './renderDocument'; diff --git a/extensions/markdown-language-features/src/commands/renderDocument.ts b/extensions/markdown-language-features/src/commands/renderDocument.ts new file mode 100644 index 0000000000000..f9ec89fce3d3e --- /dev/null +++ b/extensions/markdown-language-features/src/commands/renderDocument.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command } from '../commandManager'; +import { MarkdownEngine } from '../markdownEngine'; +import { SkinnyTextDocument } from '../tableOfContentsProvider'; + +export class RenderDocument implements Command { + public readonly id = 'markdown.api.render'; + + public constructor( + private readonly engine: MarkdownEngine + ) { } + + public async execute(document: SkinnyTextDocument | string): Promise { + return this.engine.render(document); + } +} diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 3fe2c9d4cd33d..44e8873b16dda 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -83,6 +83,7 @@ function registerMarkdownCommands( commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager)); commandManager.register(new commands.OpenDocumentLinkCommand(engine)); commandManager.register(new commands.ToggleLockCommand(previewManager)); + commandManager.register(new commands.RenderDocument(engine)); return commandManager; } diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index c874f5791edcf..3ee4d26f4f20e 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -89,6 +89,7 @@ export class MarkdownPreview extends Disposable { private isScrolling = false; private _disposed: boolean = false; private imageInfo: { id: string, width: number, height: number }[] = []; + private scrollToFragment: string | undefined; public static async revive( webview: vscode.WebviewPanel, @@ -171,19 +172,19 @@ export class MarkdownPreview extends Disposable { this._locked = locked; this.editor = webview; - this.editor.onDidDispose(() => { + this._register(this.editor.onDidDispose(() => { this.dispose(); - }, null, this._disposables); + })); - this.editor.onDidChangeViewState(e => { + this._register(this.editor.onDidChangeViewState(e => { this._onDidChangeViewStateEmitter.fire(e); - }, null, this._disposables); + })); - _contributionProvider.onContributionsChanged(() => { + this._register(_contributionProvider.onContributionsChanged(() => { setImmediate(() => this.refresh()); - }, null, this._disposables); + })); - this.editor.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { + this._register(this.editor.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { if (e.source !== this._resource.toString()) { return; } @@ -213,21 +214,21 @@ export class MarkdownPreview extends Disposable { vscode.window.showWarningMessage(localize('onPreviewStyleLoadError', "Could not load 'markdown.styles': {0}", e.body.unloadedStyles.join(', '))); break; } - }, null, this._disposables); + })); - vscode.workspace.onDidChangeTextDocument(event => { + this._register(vscode.workspace.onDidChangeTextDocument(event => { if (this.isPreviewOf(event.document.uri)) { this.refresh(); } - }, null, this._disposables); + })); - topmostLineMonitor.onDidChangeTopmostLine(event => { + this._register(topmostLineMonitor.onDidChangeTopmostLine(event => { if (this.isPreviewOf(event.resource)) { this.updateForView(event.resource, event.line); } - }, null, this._disposables); + })); - vscode.window.onDidChangeTextEditorSelection(event => { + this._register(vscode.window.onDidChangeTextEditorSelection(event => { if (this.isPreviewOf(event.textEditor.document.uri)) { this.postMessage({ type: 'onDidChangeTextEditorSelection', @@ -235,19 +236,19 @@ export class MarkdownPreview extends Disposable { source: this.resource.toString() }); } - }, null, this._disposables); + })); - vscode.window.onDidChangeActiveTextEditor(editor => { + this._register(vscode.window.onDidChangeActiveTextEditor(editor => { if (editor && isMarkdownFile(editor.document) && !this._locked) { this.update(editor.document.uri); } - }, null, this._disposables); + })); } - private readonly _onDisposeEmitter = new vscode.EventEmitter(); + private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter()); public readonly onDispose = this._onDisposeEmitter.event; - private readonly _onDidChangeViewStateEmitter = new vscode.EventEmitter(); + private readonly _onDidChangeViewStateEmitter = this._register(new vscode.EventEmitter()); public readonly onDidChangeViewState = this._onDidChangeViewStateEmitter.event; public get resource(): vscode.Uri { @@ -264,7 +265,8 @@ export class MarkdownPreview extends Disposable { locked: this._locked, line: this.line, resourceColumn: this.resourceColumn, - imageInfo: this.imageInfo + imageInfo: this.imageInfo, + fragment: this.scrollToFragment }; } @@ -284,8 +286,12 @@ export class MarkdownPreview extends Disposable { public update(resource: vscode.Uri) { const editor = vscode.window.activeTextEditor; + // Reposition scroll preview, position scroll to the top if active text editor + // doesn't corresponds with preview if (editor && editor.document.uri.fsPath === resource.fsPath) { this.line = getVisibleLine(editor); + } else { + this.line = 0; } // If we have changed resources, cancel any pending updates @@ -433,8 +439,8 @@ export class MarkdownPreview extends Disposable { if (this._resource === markdownResource) { const self = this; const resourceProvider: WebviewResourceProvider = { - toWebviewResource: (resource) => { - return this.editor.webview.toWebviewResource(normalizeResource(markdownResource, resource)); + asWebviewUri: (resource) => { + return this.editor.webview.asWebviewUri(normalizeResource(markdownResource, resource)); }, get cspSource() { return self.editor.webview.cspSource; } }; @@ -520,11 +526,15 @@ export class MarkdownPreview extends Disposable { } private async onDidClickPreviewLink(path: string, fragment: string | undefined) { + this.scrollToFragment = undefined; const config = vscode.workspace.getConfiguration('markdown', this.resource); const openLinks = config.get('preview.openMarkdownLinks', 'inPreview'); if (openLinks === 'inPreview') { const markdownLink = await resolveLinkToMarkdownFile(path); if (markdownLink) { + if (fragment) { + this.scrollToFragment = fragment; + } this.update(markdownLink); return; } diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 17b6d4f4ebb2e..0be66d1d63043 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -65,7 +65,7 @@ export class MarkdownContentProvider { scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(), - webviewResourceRoot: resourceProvider.toWebviewResource(markdownDocument.uri).toString(), + webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(), }; this.logger.log('provideTextDocumentContent', initialData); @@ -86,7 +86,7 @@ export class MarkdownContentProvider { data-state="${escapeAttribute(JSON.stringify(state || {}))}"> ${this.getStyles(resourceProvider, sourceUri, config, state)} - + ${body} @@ -110,7 +110,7 @@ export class MarkdownContentProvider { } private extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { - const webviewResource = resourceProvider.toWebviewResource( + const webviewResource = resourceProvider.asWebviewUri( vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile)))); return webviewResource.toString(); } @@ -126,17 +126,17 @@ export class MarkdownContentProvider { // Assume it must be a local file if (path.isAbsolute(href)) { - return resourceProvider.toWebviewResource(vscode.Uri.file(href)).toString(); + return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString(); } // Use a workspace relative path if there is a workspace const root = vscode.workspace.getWorkspaceFolder(resource); if (root) { - return resourceProvider.toWebviewResource(vscode.Uri.file(path.join(root.uri.fsPath, href))).toString(); + return resourceProvider.asWebviewUri(vscode.Uri.file(path.join(root.uri.fsPath, href))).toString(); } // Otherwise look relative to the markdown file - return resourceProvider.toWebviewResource(vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString(); + return resourceProvider.asWebviewUri(vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString(); } private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { @@ -176,7 +176,7 @@ export class MarkdownContentProvider { private getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, state?: any): string { const baseStyles: string[] = []; for (const resource of this.contributionProvider.contributions.previewStyles) { - baseStyles.push(``); + baseStyles.push(``); } return `${baseStyles.join('\n')} @@ -188,7 +188,7 @@ export class MarkdownContentProvider { const out: string[] = []; for (const resource of this.contributionProvider.contributions.previewScripts) { out.push(``); } @@ -209,7 +209,7 @@ export class MarkdownContentProvider { return ``; case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent: - return ''; + return ''; case MarkdownPreviewSecurityLevel.Strict: default: diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 0cc75bfe823ea..bcd7dc43ff4ba 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -118,7 +118,7 @@ export class MarkdownEngine { return md; } - private tokenize( + private tokenizeDocument( document: SkinnyTextDocument, config: MarkdownItConfig, engine: MarkdownIt @@ -131,16 +131,23 @@ export class MarkdownEngine { this.currentDocument = document.uri; this._slugCount = new Map(); - const text = document.getText(); - const tokens = engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); + const tokens = this.tokenizeString(document.getText(), engine); this._tokenCache.update(document, config, tokens); return tokens; } - public async render(document: SkinnyTextDocument): Promise { - const config = this.getConfig(document.uri); + private tokenizeString(text: string, engine: MarkdownIt) { + return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); + } + + public async render(input: SkinnyTextDocument | string): Promise { + const config = this.getConfig(typeof input === 'string' ? undefined : input.uri); const engine = await this.getEngine(config); - return engine.renderer.render(this.tokenize(document, config, engine), { + const tokens = typeof input === 'string' + ? this.tokenizeString(input, engine) + : this.tokenizeDocument(input, config, engine); + + return engine.renderer.render(tokens, { ...(engine as any).options, ...config }, {}); @@ -149,14 +156,14 @@ export class MarkdownEngine { public async parse(document: SkinnyTextDocument): Promise { const config = this.getConfig(document.uri); const engine = await this.getEngine(config); - return this.tokenize(document, config, engine); + return this.tokenizeDocument(document, config, engine); } public cleanCache(): void { this._tokenCache.clean(); } - private getConfig(resource: vscode.Uri): MarkdownItConfig { + private getConfig(resource?: vscode.Uri): MarkdownItConfig { const config = vscode.workspace.getConfiguration('markdown', resource); return { breaks: config.get('preview.breaks', false), @@ -297,13 +304,13 @@ async function getMarkdownOptions(md: () => MarkdownIt) { html: true, highlight: (str: string, lang?: string) => { // Workaround for highlight not supporting tsx: https://github.com/isagalaev/highlight.js/issues/1155 - if (lang && ['tsx', 'typescriptreact'].indexOf(lang.toLocaleLowerCase()) >= 0) { + if (lang && ['tsx', 'typescriptreact'].includes(lang.toLocaleLowerCase())) { lang = 'jsx'; } if (lang && lang.toLocaleLowerCase() === 'json5') { lang = 'json'; } - if (lang && lang.toLocaleLowerCase() === 'c#') { + if (lang && ['c#', 'csharp'].includes(lang.toLocaleLowerCase())) { lang = 'cs'; } if (lang && hljs.getLanguage(lang)) { diff --git a/extensions/markdown-language-features/src/test/engine.test.ts b/extensions/markdown-language-features/src/test/engine.test.ts new file mode 100644 index 0000000000000..3cd4f86693e5b --- /dev/null +++ b/extensions/markdown-language-features/src/test/engine.test.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as vscode from 'vscode'; +import 'mocha'; + +import { InMemoryDocument } from './inMemoryDocument'; +import { createNewMarkdownEngine } from './engine'; + +const testFileName = vscode.Uri.file('test.md'); + +suite('markdown.engine', () => { + suite('rendering', () => { + const input = '# hello\n\nworld!'; + const output = '

hello

\n' + + '

world!

\n'; + + test('Renders a document', async () => { + const doc = new InMemoryDocument(testFileName, input); + const engine = createNewMarkdownEngine(); + assert.strictEqual(await engine.render(doc), output); + }); + + test('Renders a string', async () => { + const engine = createNewMarkdownEngine(); + assert.strictEqual(await engine.render(input), output); + }); + }); +}); diff --git a/extensions/markdown-language-features/src/util/resources.ts b/extensions/markdown-language-features/src/util/resources.ts index 1def7adcce005..063c410b39eea 100644 --- a/extensions/markdown-language-features/src/util/resources.ts +++ b/extensions/markdown-language-features/src/util/resources.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; export interface WebviewResourceProvider { - toWebviewResource(resource: vscode.Uri): vscode.Uri; + asWebviewUri(resource: vscode.Uri): vscode.Uri; readonly cspSource: string; } @@ -30,4 +30,4 @@ export function normalizeResource( } } return resource; -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index a1b876e7dc8a8..a45f00885e8bd 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -3900,10 +3900,10 @@ markdown-it-front-matter@^0.1.2: resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.1.2.tgz#e50bf56e77e6a4f5ac4ffa894d4d45ccd9896b20" integrity sha1-5Qv1bnfmpPWsT/qJTU1FzNmJayA= -markdown-it@^8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" - integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== +markdown-it@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-9.1.0.tgz#df9601c168568704d554b1fff9af0c5b561168d9" + integrity sha512-xHKG4C8iPriyfu/jc2hsCC045fKrMQ0VexX2F1FGYiRxDxqMB2aAhF8WauJ3fltn2kb90moGBkiiEdooGIg55w== dependencies: argparse "^1.0.7" entities "~1.1.1" diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 843255c2141ad..a987d6082b46b 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -18,7 +18,7 @@ "watch": "gulp watch-extension:npm" }, "dependencies": { - "jsonc-parser": "^2.0.2", + "jsonc-parser": "^2.1.1", "minimatch": "^3.0.4", "request-light": "^0.2.4", "vscode-nls": "^4.0.0" diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index e80a4ca5a2daa..0f34421c9aaed 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -72,10 +72,10 @@ https-proxy-agent@^2.2.1: agent-base "^4.1.0" debug "^3.1.0" -jsonc-parser@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" - integrity sha512-TSU435K5tEKh3g7bam1AFf+uZrISheoDsLlpmAo6wWZYqjsnd09lHYK1Qo+moK4Ikifev1Gdpa69g4NELKnCrQ== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== minimatch@^3.0.4: version "3.0.4" diff --git a/extensions/package.json b/extensions/package.json index 52ece5e2051d0..5c73fcbd3d7cf 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.5.2" + "typescript": "3.6.1-rc" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index 6819e853339aa..3e14951e34228 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -32,7 +32,8 @@ let nonBuiltInLanguages = { // { fileNames, extensions } "haml": { extensions: ['haml'] }, "stylus": { extensions: ['styl'] }, "vala": { extensions: ['vala'] }, - "todo": { fileNames: ['todo'] } + "todo": { fileNames: ['todo'] }, + "jsonc": { extensions: ['json'] } }; let FROM_DISK = true; // set to true to take content from a repo checked out next to the vscode repo @@ -109,7 +110,7 @@ function downloadBinary(source, dest) { return new Promise((c, e) => { https.get(source, function (response) { switch (response.statusCode) { - case 200: + case 200: { let file = fs.createWriteStream(dest); response.on('data', function (chunk) { file.write(chunk); @@ -121,6 +122,7 @@ function downloadBinary(source, dest) { e(err.message); }); break; + } case 301: case 302: case 303: diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index c742c019ea339..1c86b8bcb2bc5 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "904c16acced1134a81b31d71d60293288c31334b" + "commitHash": "85a222708824c6f19bbecbec71633d2c97077dad" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index 52a81da4596d7..825ac52ee830c 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -1564,6 +1564,7 @@ "version.md": "_clock", "version": "_clock", "mvnw": "_maven", + "tsconfig.json": "_tsconfig", "swagger.json": "_json_1", "swagger.yml": "_json_1", "swagger.yaml": "_json_1", @@ -1573,6 +1574,8 @@ "docker-healthcheck": "_docker_2", "docker-compose.yml": "_docker_3", "docker-compose.yaml": "_docker_3", + "docker-compose.override.yml": "_docker_3", + "docker-compose.override.yaml": "_docker_3", "firebase.json": "_firebase", "geckodriver": "_firefox", "gruntfile.js": "_grunt", @@ -1940,6 +1943,7 @@ "version.md": "_clock_light", "version": "_clock_light", "mvnw": "_maven_light", + "tsconfig.json": "_tsconfig_light", "swagger.json": "_json_1_light", "swagger.yml": "_json_1_light", "swagger.yaml": "_json_1_light", @@ -1949,6 +1953,8 @@ "docker-healthcheck": "_docker_2_light", "docker-compose.yml": "_docker_3_light", "docker-compose.yaml": "_docker_3_light", + "docker-compose.override.yml": "_docker_3_light", + "docker-compose.override.yaml": "_docker_3_light", "firebase.json": "_firebase_light", "geckodriver": "_firefox_light", "gruntfile.js": "_grunt_light", @@ -1980,5 +1986,5 @@ "npm-debug.log": "_npm_ignored_light" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/904c16acced1134a81b31d71d60293288c31334b" -} + "version": "https://github.com/jesseweed/seti-ui/commit/85a222708824c6f19bbecbec71633d2c97077dad" +} \ No newline at end of file diff --git a/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json b/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json index b7b4db2d2a54a..02053ebab1365 100644 --- a/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json @@ -11,10 +11,13 @@ "while": "(^|\\G)\\s*\\*(?!/)(?=([^*]|[*](?!/))*$)", "patterns": [ { - "include": "text.html.markdown#fenced_code_block" + "include": "text.html.markdown#fenced_code_block_js" }, { - "include": "text.html.markdown#lists" + "include": "text.html.markdown#fenced_code_block_ts" + }, + { + "include": "text.html.markdown#fenced_code_block_unknown" }, { "include": "#example" @@ -47,4 +50,4 @@ } }, "scopeName": "documentation.injection" -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index fc003585c9bb0..3b9917c412cf1 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -16,7 +16,7 @@ "Programming Languages" ], "dependencies": { - "jsonc-parser": "^2.0.1", + "jsonc-parser": "^2.1.1", "rimraf": "^2.6.3", "semver": "5.5.1", "vscode-extension-telemetry": "0.1.1", @@ -758,4 +758,4 @@ } ] } -} \ 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 a00a2db5f00cb..0867c86fdc755 100644 --- a/extensions/typescript-language-features/src/features/folding.ts +++ b/extensions/typescript-language-features/src/features/folding.ts @@ -55,7 +55,7 @@ class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider { const start = range.start.line; // workaround for #47240 - const end = (range.end.character > 0 && document.getText(new vscode.Range(range.end.translate(0, -1), range.end)) === '}') + const end = (range.end.character > 0 && new Set(['}', ']']).has(document.getText(new vscode.Range(range.end.translate(0, -1), range.end)))) ? Math.max(range.end.line - 1, range.start.line) : range.end.line; @@ -81,4 +81,4 @@ export function register( return vscode.languages.registerFoldingRangeProvider(selector, new TypeScriptFoldingProvider(client)); }); -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/src/features/quickFix.ts b/extensions/typescript-language-features/src/features/quickFix.ts index 6ce9f199704e1..2585142e0972a 100644 --- a/extensions/typescript-language-features/src/features/quickFix.ts +++ b/extensions/typescript-language-features/src/features/quickFix.ts @@ -286,7 +286,13 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { } // Make sure there are multiple diagnostics of the same type in the file - if (!this.diagnosticsManager.getDiagnostics(document.uri).some(x => x.code === diagnostic.code && x !== diagnostic)) { + if (!this.diagnosticsManager.getDiagnostics(document.uri).some(x => { + if (x === diagnostic) { + return false; + } + return x.code === diagnostic.code + || (fixAllErrorCodes.has(x.code as number) && fixAllErrorCodes.get(x.code as number) === fixAllErrorCodes.get(diagnostic.code as number)); + })) { return results; } @@ -304,6 +310,15 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { } } +// Some fix all actions can actually fix multiple differnt diagnostics. Make sure we still show the fix all action +// in such cases +const fixAllErrorCodes = new Map([ + // Missing async + [2339, 2339], + [2345, 2339], +]); + + const preferredFixes = new Set([ 'annotateWithTypeFromJSDoc', 'constructorForDerivedNeedSuperCall', diff --git a/extensions/typescript-language-features/src/features/refactor.ts b/extensions/typescript-language-features/src/features/refactor.ts index 6b0d33a2bb99e..f4ed0f6726f64 100644 --- a/extensions/typescript-language-features/src/features/refactor.ts +++ b/extensions/typescript-language-features/src/features/refactor.ts @@ -14,7 +14,7 @@ import { VersionDependentRegistration } from '../utils/dependentRegistration'; import TelemetryReporter from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; import FormattingOptionsManager from './fileConfigurationManager'; -import { file } from '../utils/fileSchemes'; +import * as fileSchemes from '../utils/fileSchemes'; const localize = nls.loadMessageBundle(); @@ -30,11 +30,15 @@ class ApplyRefactoringCommand implements Command { public async execute( document: vscode.TextDocument, - file: string, refactor: string, action: string, range: vscode.Range ): Promise { + const file = this.client.toOpenedFilePath(document); + if (!file) { + return false; + } + /* __GDPR__ "refactor.execute" : { "action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, @@ -81,7 +85,7 @@ class ApplyRefactoringCommand implements Command { const workspaceEdit = new vscode.WorkspaceEdit(); for (const edit of body.edits) { const resource = this.client.toResource(edit.fileName); - if (resource.scheme === file) { + if (resource.scheme === fileSchemes.file) { workspaceEdit.createFile(resource, { ignoreIfExists: true }); } } @@ -95,15 +99,19 @@ class SelectRefactorCommand implements Command { public readonly id = SelectRefactorCommand.ID; constructor( + private readonly client: ITypeScriptServiceClient, private readonly doRefactoring: ApplyRefactoringCommand ) { } public async execute( document: vscode.TextDocument, - file: string, info: Proto.ApplicableRefactorInfo, range: vscode.Range ): Promise { + const file = this.client.toOpenedFilePath(document); + if (!file) { + return false; + } const selected = await vscode.window.showQuickPick(info.actions.map((action): vscode.QuickPickItem => ({ label: action.name, description: action.description, @@ -111,7 +119,7 @@ class SelectRefactorCommand implements Command { if (!selected) { return false; } - return this.doRefactoring.execute(document, file, info.name, selected.label, range); + return this.doRefactoring.execute(document, info.name, selected.label, range); } } @@ -130,7 +138,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { telemetryReporter: TelemetryReporter ) { const doRefactoringCommand = commandManager.register(new ApplyRefactoringCommand(this.client, telemetryReporter)); - commandManager.register(new SelectRefactorCommand(doRefactoringCommand)); + commandManager.register(new SelectRefactorCommand(this.client, doRefactoringCommand)); } public static readonly metadata: vscode.CodeActionProviderMetadata = { @@ -146,29 +154,30 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { if (!this.shouldTrigger(rangeOrSelection, context)) { return undefined; } - - const file = this.client.toOpenedFilePath(document); - if (!file) { + if (!this.client.toOpenedFilePath(document)) { return undefined; } - const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs(file, rangeOrSelection); + const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs(fileSchemes.file, rangeOrSelection); const response = await this.client.interruptGetErr(() => { + const file = this.client.toOpenedFilePath(document); + if (!file) { + return undefined; + } this.formattingOptionsManager.ensureConfigurationForDocument(document, token); return this.client.execute('getApplicableRefactors', args, token); }); - if (response.type !== 'response' || !response.body) { + if (!response || response.type !== 'response' || !response.body) { return undefined; } - return this.convertApplicableRefactors(response.body, document, file, rangeOrSelection); + return this.convertApplicableRefactors(response.body, document, rangeOrSelection); } private convertApplicableRefactors( body: Proto.ApplicableRefactorInfo[], document: vscode.TextDocument, - file: string, rangeOrSelection: vscode.Range | vscode.Selection ) { const actions: vscode.CodeAction[] = []; @@ -178,12 +187,12 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { codeAction.command = { title: info.description, command: SelectRefactorCommand.ID, - arguments: [document, file, info, rangeOrSelection] + arguments: [document, info, rangeOrSelection] }; actions.push(codeAction); } else { for (const action of info.actions) { - actions.push(this.refactorActionToCodeAction(action, document, file, info, rangeOrSelection)); + actions.push(this.refactorActionToCodeAction(action, document, info, rangeOrSelection)); } } } @@ -193,7 +202,6 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { private refactorActionToCodeAction( action: Proto.RefactorActionInfo, document: vscode.TextDocument, - file: string, info: Proto.ApplicableRefactorInfo, rangeOrSelection: vscode.Range | vscode.Selection ) { @@ -201,7 +209,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { codeAction.command = { title: action.description, command: ApplyRefactoringCommand.ID, - arguments: [document, file, info.name, action.name, rangeOrSelection], + arguments: [document, info.name, action.name, rangeOrSelection], }; codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action); return codeAction; diff --git a/extensions/typescript-language-features/src/features/task.ts b/extensions/typescript-language-features/src/features/task.ts index e47ee6d7fdcb9..64057555d5f73 100644 --- a/extensions/typescript-language-features/src/features/task.ts +++ b/extensions/typescript-language-features/src/features/task.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; +import * as jsonc from 'jsonc-parser'; import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import * as jsonc from 'jsonc-parser'; import { ITypeScriptServiceClient } from '../typescriptService'; +import { isTsConfigFileName } from '../utils/languageDescription'; import { Lazy } from '../utils/lazy'; import { isImplicitProjectConfigFile } from '../utils/tsconfig'; import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider'; @@ -113,7 +114,7 @@ export default class TscTaskProvider implements vscode.TaskProvider { private async getTsConfigForActiveFile(token: vscode.CancellationToken): Promise { const editor = vscode.window.activeTextEditor; if (editor) { - if (path.basename(editor.document.fileName).match(/^tsconfig\.(.\.)?json$/)) { + if (isTsConfigFileName(editor.document.fileName)) { const uri = editor.document.uri; return [{ path: uri.fsPath, diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index c4c2e9ca13565..102658ecf0121 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -33,14 +33,14 @@ function isDirectory(path: string): Promise { }); } -enum UpdateImportsOnFileMoveSetting { +const enum UpdateImportsOnFileMoveSetting { Prompt = 'prompt', Always = 'always', Never = 'never', } class UpdateImportsOnFileRenameHandler extends Disposable { - public static minVersion = API.v300; + public static readonly minVersion = API.v300; public constructor( private readonly client: ITypeScriptServiceClient, @@ -127,7 +127,7 @@ class UpdateImportsOnFileRenameHandler extends Disposable { newResource: vscode.Uri, newDocument: vscode.TextDocument ): Promise { - enum Choice { + const enum Choice { None = 0, Accept = 1, Reject = 2, @@ -237,4 +237,4 @@ export function register( ) { return new VersionDependentRegistration(client, UpdateImportsOnFileRenameHandler.minVersion, () => new UpdateImportsOnFileRenameHandler(client, fileConfigurationManager, handles)); -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/src/tsServer/serverError.ts b/extensions/typescript-language-features/src/tsServer/serverError.ts index cb5cb8035f1e4..b51ca545bff0a 100644 --- a/extensions/typescript-language-features/src/tsServer/serverError.ts +++ b/extensions/typescript-language-features/src/tsServer/serverError.ts @@ -40,7 +40,16 @@ export class TypeScriptServerError extends Error { if (errorText) { const errorPrefix = 'Error processing request. '; if (errorText.startsWith(errorPrefix)) { - const prefixFreeErrorText = errorText.substr(errorPrefix.length); + let prefixFreeErrorText = errorText.substr(errorPrefix.length); + + // Prior to https://github.com/microsoft/TypeScript/pull/32785, this error + // returned and excessively long and detailed list of paths. Since server-side + // filtering doesn't have sufficient granularity to drop these specific + // messages, we sanitize them here. + if (prefixFreeErrorText.indexOf('Could not find sourceFile') >= 0) { + prefixFreeErrorText = prefixFreeErrorText.replace(/ in \[[^\]]*\]/g, ''); + } + const newlineIndex = prefixFreeErrorText.indexOf('\n'); if (newlineIndex >= 0) { // Newline expected between message and stack. diff --git a/extensions/typescript-language-features/src/utils/languageDescription.ts b/extensions/typescript-language-features/src/utils/languageDescription.ts index 79f9ce9f47b0a..f6b066bd40011 100644 --- a/extensions/typescript-language-features/src/utils/languageDescription.ts +++ b/extensions/typescript-language-features/src/utils/languageDescription.ts @@ -2,6 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { basename } from 'path'; import * as languageModeIds from './languageModeIds'; export const enum DiagnosticLanguage { @@ -38,3 +40,11 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ configFilePattern: /^jsconfig(\..*)?\.json$/gi } ]; + +export function isTsConfigFileName(fileName: string): boolean { + return /^tsconfig\.(.+\.)?json$/i.test(basename(fileName)); +} + +export function isJsConfigOrTsConfigFileName(fileName: string): boolean { + return /^[jt]sconfig\.(.+\.)?json$/i.test(basename(fileName)); +} diff --git a/extensions/typescript-language-features/src/utils/managedFileContext.ts b/extensions/typescript-language-features/src/utils/managedFileContext.ts index 81acf9ad50500..481f2482d2631 100644 --- a/extensions/typescript-language-features/src/utils/managedFileContext.ts +++ b/extensions/typescript-language-features/src/utils/managedFileContext.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { isSupportedLanguageMode } from './languageModeIds'; import { Disposable } from './dispose'; +import { isJsConfigOrTsConfigFileName } from './languageDescription'; +import { isSupportedLanguageMode } from './languageModeIds'; /** * When clause context set when the current file is managed by vscode's built-in typescript extension. @@ -26,8 +27,7 @@ export default class ManagedFileContextManager extends Disposable { private onDidChangeActiveTextEditor(editor?: vscode.TextEditor): any { if (editor) { - const isManagedFile = isSupportedLanguageMode(editor.document) && this.normalizePath(editor.document.uri) !== null; - this.updateContext(isManagedFile); + this.updateContext(this.isManagedFile(editor)); } } @@ -39,4 +39,16 @@ export default class ManagedFileContextManager extends Disposable { vscode.commands.executeCommand('setContext', ManagedFileContextManager.contextName, newValue); this.isInManagedFileContext = newValue; } + + private isManagedFile(editor: vscode.TextEditor): boolean { + return this.isManagedScriptFile(editor) || this.isManagedConfigFile(editor); + } + + private isManagedScriptFile(editor: vscode.TextEditor): boolean { + return isSupportedLanguageMode(editor.document) && this.normalizePath(editor.document.uri) !== null; + } + + private isManagedConfigFile(editor: vscode.TextEditor): boolean { + return isJsConfigOrTsConfigFileName(editor.document.fileName); + } } diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 683f002c7278d..682bc093c6c10 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -1027,10 +1027,10 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -jsonc-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.1.tgz#9d23cd2709714fff508a1a6679d82135bee1ae60" - integrity sha512-9w/QyN9qF1dTlffzkmyITa6qAYt6sDArlVZqaP+CXnRh66V73wImQGG8GIBkuas0XLAxddWEWYQ8PPFoK5KNQA== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== jsonify@~0.0.0: version "0.0.0" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts index 9ea76e6a1193b..7dba2cfa93c30 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts @@ -113,31 +113,4 @@ suite('commands namespace tests', () => { return Promise.all([a, b, c, d]); }); - - test('onDidExecuteCommand', async function () { - let args: any[]; - let d1 = commands.registerCommand('t1', function () { - args = [...arguments]; - }); - - - const p = new Promise((resolve, reject) => { - - let d2 = commands.onDidExecuteCommand(event => { - d2.dispose(); - d1.dispose(); - - try { - assert.equal(event.command, 't1'); - assert.deepEqual(args, event.arguments); - resolve(); - } catch (e) { - reject(e); - } - }); - }); - - await commands.executeCommand('t1', { foo: 1 }); - await p; - }); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts index 4be0218ab6435..e785f1d4afbe1 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts @@ -251,18 +251,18 @@ suite('Webview tests', () => { }); `); - async function toWebviewResource(path: string) { - const root = await webview.webview.toWebviewResource(vscode.Uri.file(vscode.workspace.rootPath!)); + async function asWebviewUri(path: string) { + const root = await webview.webview.asWebviewUri(vscode.Uri.file(vscode.workspace.rootPath!)); return root.toString() + path; } { - const imagePath = await toWebviewResource('/image.png'); + const imagePath = await asWebviewUri('/image.png'); const response = sendRecieveMessage(webview, { src: imagePath }); assert.strictEqual((await response).value, true); } { - const imagePath = await toWebviewResource('/no-such-image.png'); + const imagePath = await asWebviewUri('/no-such-image.png'); const response = sendRecieveMessage(webview, { src: imagePath }); assert.strictEqual((await response).value, false); } diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 8e552194c13ec..26979cdf9d679 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.2.tgz#a09e1dc69bc9551cadf17dba10ee42cf55e5d56c" - integrity sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA== +typescript@3.6.1-rc: + version "3.6.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.1-rc.tgz#9db650b25d8ef033d9e25b3057bdd1e102bb434b" + integrity sha512-u6AQN9AoocZKYSz8zcc1Qh/V/mbAO+BHc73fTiKlIdjzU60A8TesrK9/7kg3GM8o2RxNyCeOFpcevEtnfUyaLg== diff --git a/package.json b/package.json index 076ad75b4b7a7..e489e9b063185 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.38.0", - "distro": "462afd2bf9dd37b39205412486925bc10b7fbf69", + "distro": "30060f8ffc31f3de68146de3df305a71973ae1db", "author": { "name": "Microsoft Corporation" }, @@ -28,6 +28,7 @@ "update-distro": "node build/npm/update-distro.js" }, "dependencies": { + "@microsoft/applicationinsights-web": "^2.1.1", "applicationinsights": "1.0.8", "graceful-fs": "4.1.11", "http-proxy-agent": "^2.1.0", @@ -51,14 +52,15 @@ "vscode-ripgrep": "^1.5.6", "vscode-sqlite3": "4.0.8", "vscode-textmate": "^4.2.2", - "xterm": "3.15.0-beta98", - "xterm-addon-search": "0.2.0-beta3", + "xterm": "3.15.0-beta101", + "xterm-addon-search": "0.2.0-beta5", "xterm-addon-web-links": "0.1.0-beta10", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, "devDependencies": { "7zip": "0.0.6", + "@types/cookie": "^0.3.3", "@types/keytar": "^4.4.0", "@types/mocha": "2.2.39", "@types/node": "^10.12.12", @@ -82,7 +84,7 @@ "fast-plist": "0.1.2", "glob": "^5.0.13", "gulp": "^4.0.0", - "gulp-atom-electron": "^1.21.1", + "gulp-atom-electron": "^1.22.0", "gulp-azure-storage": "^0.10.0", "gulp-buffer": "0.0.2", "gulp-concat": "^2.6.1", @@ -123,6 +125,7 @@ "optimist": "0.3.5", "p-all": "^1.0.0", "pump": "^1.0.1", + "puppeteer": "^1.19.0", "queue": "3.0.6", "rcedit": "^1.1.0", "rimraf": "^2.2.8", @@ -137,7 +140,7 @@ "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vsce": "1.48.0", - "vscode-debugprotocol": "1.35.0", + "vscode-debugprotocol": "1.36.0-pre.0", "vscode-nls-dev": "^3.3.1", "webpack": "^4.16.5", "webpack-cli": "^3.1.0", @@ -157,4 +160,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} \ No newline at end of file +} diff --git a/remote/package.json b/remote/package.json index 4547f9eed803a..9fa3881681da4 100644 --- a/remote/package.json +++ b/remote/package.json @@ -2,6 +2,7 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { + "@microsoft/applicationinsights-web": "^2.1.1", "applicationinsights": "1.0.8", "getmac": "1.4.1", "graceful-fs": "4.1.11", @@ -18,16 +19,16 @@ "vscode-chokidar": "2.1.7", "vscode-minimist": "^1.2.1", "vscode-proxy-agent": "0.4.0", - "vscode-ripgrep": "^1.5.5", + "vscode-ripgrep": "^1.5.6", "vscode-textmate": "^4.2.2", - "xterm": "3.15.0-beta98", - "xterm-addon-search": "0.2.0-beta3", + "xterm": "3.15.0-beta101", + "xterm-addon-search": "0.2.0-beta5", "xterm-addon-web-links": "0.1.0-beta10", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, "optionalDependencies": { "vscode-windows-ca-certs": "0.1.0", - "vscode-windows-registry": "1.0.1" + "vscode-windows-registry": "1.0.2" } } diff --git a/remote/web/.yarnrc b/remote/web/.yarnrc deleted file mode 100644 index b28191e6bae4c..0000000000000 --- a/remote/web/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "http://nodejs.org/dist" -target "10.11.0" -runtime "node" diff --git a/remote/web/package.json b/remote/web/package.json index c7a487b6c2d3d..f54afe1a3e6e8 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -2,6 +2,7 @@ "name": "vscode-web", "version": "0.0.0", "dependencies": { + "@microsoft/applicationinsights-web": "^2.1.1", "onigasm-umd": "^2.2.2", "vscode-textmate": "^4.1.1", "xterm": "3.15.0-beta67", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index b624eb3729027..23ad784a554f5 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -2,6 +2,69 @@ # yarn lockfile v1 +"@microsoft/applicationinsights-analytics-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.1.1.tgz#6d09c1915f808026e2d45165d04802f09affed59" + integrity sha512-VKIutoFKY99CyKwxLUuj6Vnq14/QwXo9/QSQDpYnHEjo+uKn7QmLsHqWw0K9uYNfNAXt4BZimX/zDg6jZtzeXg== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-channel-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.1.1.tgz#e205eddd93e49d17d9e0711a612b4bfc9810888f" + integrity sha512-fYr9IAqtaEr9AmaPaL3SLQVT3t3GQzl+n74gpNKyAVakDIm0nYQ/bimjdcAhJMDf1VGNSPg/xICneyuZg7Wxlg== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-common@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-2.1.1.tgz#27e6074584a7a3a8ca3f11f7ff2b7ff0f395bf2d" + integrity sha512-2hkS1Ia1FmAjCuYZ5JlG20/WgObqdsKtmK5YALAFGHIB4KSQ/Za1qazS+7GsG+E0F9UJivNWL1geUIcNqg5Qjg== + dependencies: + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-core-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.1.1.tgz#30fb6a519cc1c6119c419c4811ce72c260217d9e" + integrity sha512-4t4wf6SKqIcWEQDPg/uOhm+BxtHhu/AFreyEoYZmMfcxzAu33h1FtTQRtxBNbYH1+thiNZCh80yUpnT7d9Hrlw== + dependencies: + tslib "^1.9.3" + +"@microsoft/applicationinsights-dependencies-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.1.1.tgz#8154c3efcb24617d015d0bce7c2cc47797a8d3c4" + integrity sha512-yhb4EToBp+aI+qLo0h5NDNtoo3sDFV60uyIOK843YjzXqVotcXX/lRShlghTkJtYH09QhrdzDjViUHnD4sMFSQ== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-properties-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.1.1.tgz#ca34232766eb16167b5d87693e2ae5d94f2a1559" + integrity sha512-8l+/ppw6xKTam2RL4EHZ52Lcf217olw81j6kyBNKtIcGwSnLNHrFwEeF3vBWIteG2JKzlg1GhGjrkB3oxXsV2g== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-web@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web/-/applicationinsights-web-2.1.1.tgz#1a44eddda7c244b88d9eb052dab6c855682e4f05" + integrity sha512-crvhCkNsNxkFuPWmttyWNSAA96D5FxBtKS6UA9MV9f9XHevTfchf/E3AuU9JZcsXufWMQLwLrUQ9ZiA1QJ0EWA== + dependencies: + "@microsoft/applicationinsights-analytics-js" "2.1.1" + "@microsoft/applicationinsights-channel-js" "2.1.1" + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + "@microsoft/applicationinsights-dependencies-js" "2.1.1" + "@microsoft/applicationinsights-properties-js" "2.1.1" + nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -24,6 +87,11 @@ semver-umd@^5.5.3: resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.3.tgz#b64d7a2d4f5a717b369d56e31940a38e47e34d1e" integrity sha512-HOnQrn2iKnVe/xlqCTzMXQdvSz3rPbD0DmQXYuQ+oK1dpptGFfPghonQrx5JHl2O7EJwDqtQnjhE7ME23q6ngw== +tslib@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + vscode-textmate@^4.1.1: version "4.2.2" resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-4.2.2.tgz#0b4dabc69a6fba79a065cb6b615f66eac07c8f4c" diff --git a/remote/yarn.lock b/remote/yarn.lock index 3e5d4690cf7a9..308bf9634b7a5 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -2,6 +2,69 @@ # yarn lockfile v1 +"@microsoft/applicationinsights-analytics-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.1.1.tgz#6d09c1915f808026e2d45165d04802f09affed59" + integrity sha512-VKIutoFKY99CyKwxLUuj6Vnq14/QwXo9/QSQDpYnHEjo+uKn7QmLsHqWw0K9uYNfNAXt4BZimX/zDg6jZtzeXg== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-channel-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.1.1.tgz#e205eddd93e49d17d9e0711a612b4bfc9810888f" + integrity sha512-fYr9IAqtaEr9AmaPaL3SLQVT3t3GQzl+n74gpNKyAVakDIm0nYQ/bimjdcAhJMDf1VGNSPg/xICneyuZg7Wxlg== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-common@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-2.1.1.tgz#27e6074584a7a3a8ca3f11f7ff2b7ff0f395bf2d" + integrity sha512-2hkS1Ia1FmAjCuYZ5JlG20/WgObqdsKtmK5YALAFGHIB4KSQ/Za1qazS+7GsG+E0F9UJivNWL1geUIcNqg5Qjg== + dependencies: + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-core-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.1.1.tgz#30fb6a519cc1c6119c419c4811ce72c260217d9e" + integrity sha512-4t4wf6SKqIcWEQDPg/uOhm+BxtHhu/AFreyEoYZmMfcxzAu33h1FtTQRtxBNbYH1+thiNZCh80yUpnT7d9Hrlw== + dependencies: + tslib "^1.9.3" + +"@microsoft/applicationinsights-dependencies-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.1.1.tgz#8154c3efcb24617d015d0bce7c2cc47797a8d3c4" + integrity sha512-yhb4EToBp+aI+qLo0h5NDNtoo3sDFV60uyIOK843YjzXqVotcXX/lRShlghTkJtYH09QhrdzDjViUHnD4sMFSQ== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-properties-js@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.1.1.tgz#ca34232766eb16167b5d87693e2ae5d94f2a1559" + integrity sha512-8l+/ppw6xKTam2RL4EHZ52Lcf217olw81j6kyBNKtIcGwSnLNHrFwEeF3vBWIteG2JKzlg1GhGjrkB3oxXsV2g== + dependencies: + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + tslib "^1.9.3" + +"@microsoft/applicationinsights-web@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web/-/applicationinsights-web-2.1.1.tgz#1a44eddda7c244b88d9eb052dab6c855682e4f05" + integrity sha512-crvhCkNsNxkFuPWmttyWNSAA96D5FxBtKS6UA9MV9f9XHevTfchf/E3AuU9JZcsXufWMQLwLrUQ9ZiA1QJ0EWA== + dependencies: + "@microsoft/applicationinsights-analytics-js" "2.1.1" + "@microsoft/applicationinsights-channel-js" "2.1.1" + "@microsoft/applicationinsights-common" "2.1.1" + "@microsoft/applicationinsights-core-js" "2.1.1" + "@microsoft/applicationinsights-dependencies-js" "2.1.1" + "@microsoft/applicationinsights-properties-js" "2.1.1" + agent-base@4, agent-base@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -680,7 +743,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -nan@^2.0.0, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0: +nan@^2.0.0, nan@^2.13.2, nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -1024,6 +1087,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +tslib@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + typechecker@^4.3.0: version "4.7.0" resolved "https://registry.yarnpkg.com/typechecker/-/typechecker-4.7.0.tgz#5249f427358f45b7250c4924fd4d01ed9ba435e9" @@ -1123,10 +1191,10 @@ vscode-proxy-agent@0.4.0: https-proxy-agent "2.2.1" socks-proxy-agent "4.0.1" -vscode-ripgrep@^1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.5.5.tgz#24c0e9cb356cf889c98e15ecb58f9cf654a1d961" - integrity sha512-OrPrAmcun4+uZAuNcQvE6CCPskh+5AsjANod/Q3zRcJcGNxgoOSGlQN9RPtatkUNmkN8Nn8mZBnS1jMylu/dKg== +vscode-ripgrep@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.5.6.tgz#93bf5c99ca5f8248950a305e224f6ca153c30af4" + integrity sha512-WRIM9XpUj6dsfdAmuI3ANbmT1ysPUVsYy/2uCLDHJa9kbiB4T7uGvFnnc0Rgx2qQnyRAwL7PeWaFgUljPPxf2g== vscode-textmate@^4.2.2: version "4.2.2" @@ -1142,27 +1210,25 @@ vscode-windows-ca-certs@0.1.0: dependencies: node-addon-api "1.6.2" -vscode-windows-registry@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.1.tgz#bc9f765563eb6dc1c9ad9a41f9eaacc84dfadc7c" - integrity sha512-q0aKXi9Py1OBdmXIJJFeJBzpPJMMUxMJNBU9FysWIXEwJyMQGEVevKzM2J3Qz/cHSc5LVqibmoUWzZ7g+97qRg== - dependencies: - nan "^2.12.1" +vscode-windows-registry@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a" + integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA== -xterm-addon-search@0.2.0-beta3: - version "0.2.0-beta3" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.2.0-beta3.tgz#710ce14658e269c5a4791f5a9e2f520883a2e62b" - integrity sha512-KzVdkEtGbKJe9ER2TmrI7XjF/wUq1lir9U63vPJi0t2ymQvIECl1V63f9QtOp1vvpdhbZiXBxO+vGTj+y0tRow== +xterm-addon-search@0.2.0-beta5: + version "0.2.0-beta5" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.2.0-beta5.tgz#258d7cb1511d9060cd4520f0f82e408000fd4f53" + integrity sha512-Tg+d8scch0rYOVmzBbX35Y1GXtq+eu/YlzbXznmTo/yD83j3BQlXOhgECu/Yv8EX5JwFmzbfVRWC+JWnfigwGg== xterm-addon-web-links@0.1.0-beta10: version "0.1.0-beta10" resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.1.0-beta10.tgz#610fa9773a2a5ccd41c1c83ba0e2dd2c9eb66a23" integrity sha512-xfpjy0V6bB4BR44qIgZQPoCMVakxb65gMscPkHpO//QxvUxKzabV3dxOsIbeZRFkUGsWTFlvz2OoaBLoNtv5gg== -xterm@3.15.0-beta98: - version "3.15.0-beta98" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.15.0-beta98.tgz#37f37c35577422880e7ef673cc37f9d2a45dd40c" - integrity sha512-vZbg2LcRvoiJOgr1MyeLFM9mF4uib3BWUWDHyFc+vZ58CTuK0iczOvFXgk/ySo23ZLqwmHQSigLgmWvZ8J5G0Q== +xterm@3.15.0-beta101: + version "3.15.0-beta101" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.15.0-beta101.tgz#38ffa0df5a3e9bdcb1818e74fe59b2f98b0fff69" + integrity sha512-HRa7+FDqQ8iWBTvb1Ni+uMGILnu6k9mF7JHMHRHfWxFoQlSoGYCyfdyXlJjk68YN8GsEQREmrII6cPLiQizdEQ== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/completions/zsh/_code b/resources/completions/zsh/_code index 25dfe7ca98485..c7abee1bc5841 100644 --- a/resources/completions/zsh/_code +++ b/resources/completions/zsh/_code @@ -17,7 +17,7 @@ arguments=( '--telemetry[show all telemetry events which VS code collects]' '--extensions-dir[set the root path for extensions]:root path:_directories' '--list-extensions[list the installed extensions]' - '--category[filters instaled extension list by category, when using --list-extension]' + '--category[filters installed extension list by category, when using --list-extension]' '--show-versions[show versions of installed extensions, when using --list-extension]' '--install-extension[install an extension]:id or path:_files -g "*.vsix(-.)"' '--uninstall-extension[uninstall an extension]:id or path:_files -g "*.vsix(-.)"' diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index 4bc4677e1e483..745f2adb7aafc 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -11,14 +11,9 @@ VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" ELECTRON="$VSCODE_PATH/$NAME.exe" if grep -qi Microsoft /proc/version; then # in a wsl shell - if [ "$WSL_DISTRO_NAME" ]; then - # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 - WSL_BUILD=18362 - else - WSL_BUILD=$(uname -r | sed -E 's/^.+-([0-9]+)-Microsoft/\1/') - if [ -z "$WSL_BUILD" ]; then - WSL_BUILD=0 - fi + WSL_BUILD=$(uname -r | sed -E 's/^[0-9.]+-([0-9]+)-Microsoft|([0-9]+).([0-9]+).([0-9]+)-microsoft-standard|.*/\1\2\3\4/') + if [ -z "$WSL_BUILD" ]; then + WSL_BUILD=0 fi if [ $WSL_BUILD -ge 17063 ]; then @@ -30,7 +25,18 @@ if grep -qi Microsoft /proc/version; then # use the Remote WSL extension if installed WSL_EXT_ID="ms-vscode-remote.remote-wsl" - WSL_EXT_WLOC=$(ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID) + + if [ $WSL_BUILD -ge 41955 ]; then + # WSL2 in workaround for https://github.com/microsoft/WSL/issues/4337 + CWD="$(pwd)" + cd "$VSCODE_PATH" + cmd.exe /C ".\\bin\\$APP_NAME.cmd --locate-extension $WSL_EXT_ID >remote-wsl-loc.txt" + WSL_EXT_WLOC="$(cat ./remote-wsl-loc.txt)" + rm remote-wsl-loc.txt + cd "$CWD" + else + WSL_EXT_WLOC=$(ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID) + fi if [ -n "$WSL_EXT_WLOC" ]; then # replace \r\n with \n in WSL_EXT_WLOC WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh diff --git a/resources/win32/inno-big-100.bmp b/resources/win32/inno-big-100.bmp new file mode 100644 index 0000000000000..99cf4ba66683a Binary files /dev/null and b/resources/win32/inno-big-100.bmp differ diff --git a/resources/win32/inno-big.bmp b/resources/win32/inno-big-125.bmp old mode 100644 new mode 100755 similarity index 67% rename from resources/win32/inno-big.bmp rename to resources/win32/inno-big-125.bmp index d9aa14eb70899..d781943ada50c Binary files a/resources/win32/inno-big.bmp and b/resources/win32/inno-big-125.bmp differ diff --git a/resources/win32/inno-big-150.bmp b/resources/win32/inno-big-150.bmp new file mode 100755 index 0000000000000..554461982f01c Binary files /dev/null and b/resources/win32/inno-big-150.bmp differ diff --git a/resources/win32/inno-big-175.bmp b/resources/win32/inno-big-175.bmp new file mode 100755 index 0000000000000..be0e7df91cf7c Binary files /dev/null and b/resources/win32/inno-big-175.bmp differ diff --git a/resources/win32/inno-big-200.bmp b/resources/win32/inno-big-200.bmp new file mode 100755 index 0000000000000..25ecb88e83322 Binary files /dev/null and b/resources/win32/inno-big-200.bmp differ diff --git a/resources/win32/inno-big-225.bmp b/resources/win32/inno-big-225.bmp new file mode 100755 index 0000000000000..89cf4efb1e0b8 Binary files /dev/null and b/resources/win32/inno-big-225.bmp differ diff --git a/resources/win32/inno-big-250.bmp b/resources/win32/inno-big-250.bmp new file mode 100644 index 0000000000000..a791fb63ec6e3 Binary files /dev/null and b/resources/win32/inno-big-250.bmp differ diff --git a/resources/win32/inno-small-100.bmp b/resources/win32/inno-small-100.bmp new file mode 100755 index 0000000000000..59f09fd81342d Binary files /dev/null and b/resources/win32/inno-small-100.bmp differ diff --git a/resources/win32/inno-small-125.bmp b/resources/win32/inno-small-125.bmp new file mode 100755 index 0000000000000..69b3b1fa2ee5a Binary files /dev/null and b/resources/win32/inno-small-125.bmp differ diff --git a/resources/win32/inno-small-150.bmp b/resources/win32/inno-small-150.bmp new file mode 100755 index 0000000000000..3f65efc5f4c33 Binary files /dev/null and b/resources/win32/inno-small-150.bmp differ diff --git a/resources/win32/inno-small-175.bmp b/resources/win32/inno-small-175.bmp new file mode 100755 index 0000000000000..a11b53bcfe15b Binary files /dev/null and b/resources/win32/inno-small-175.bmp differ diff --git a/resources/win32/inno-small-200.bmp b/resources/win32/inno-small-200.bmp new file mode 100755 index 0000000000000..c0ad5436b66a9 Binary files /dev/null and b/resources/win32/inno-small-200.bmp differ diff --git a/resources/win32/inno-small-225.bmp b/resources/win32/inno-small-225.bmp new file mode 100755 index 0000000000000..9a974c4d9c07c Binary files /dev/null and b/resources/win32/inno-small-225.bmp differ diff --git a/resources/win32/inno-small-250.bmp b/resources/win32/inno-small-250.bmp new file mode 100755 index 0000000000000..f32a791ea8f36 Binary files /dev/null and b/resources/win32/inno-small-250.bmp differ diff --git a/resources/win32/inno-small.bmp b/resources/win32/inno-small.bmp deleted file mode 100644 index 98feb812d2542..0000000000000 Binary files a/resources/win32/inno-small.bmp and /dev/null differ diff --git a/scripts/code.bat b/scripts/code.bat index 8c058365dca7f..f4689608e4a73 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -33,6 +33,7 @@ set VSCODE_CLI=1 set ELECTRON_DEFAULT_ERROR_MODE=1 set ELECTRON_ENABLE_LOGGING=1 set ELECTRON_ENABLE_STACK_DUMPING=1 +set VSCODE_LOGS= :: Launch Code diff --git a/src/buildfile.js b/src/buildfile.js index e45dbc2746158..746975249eda4 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +function entrypoint(name) { + return [{ name: name, include: [], exclude: ['vs/css', 'vs/nls'] }]; +} + exports.base = [{ name: 'vs/base/common/worker/simpleWorker', include: ['vs/editor/common/services/editorSimpleWorker'], @@ -19,11 +23,17 @@ exports.serviceWorker = [{ dest: 'vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.js' }]; +exports.workerExtensionHost = [entrypoint('vs/workbench/services/extensions/worker/extensionHostWorker')]; + exports.workbench = require('./vs/workbench/buildfile').collectModules(['vs/workbench/workbench.desktop.main']); -exports.workbenchWeb = require('./vs/workbench/buildfile').collectModules(['vs/workbench/workbench.web.api']); +exports.workbenchWeb = entrypoint('vs/workbench/workbench.web.api'); + +exports.keyboardMaps = [ + entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'), + entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin'), + entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win') +]; exports.code = require('./vs/code/buildfile').collectModules(); -exports.entrypoint = function (name) { - return [{ name: name, include: [], exclude: ['vs/css', 'vs/nls'] }]; -}; +exports.entrypoint = entrypoint; diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 79d754c01f5c3..b782f00466ad9 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -17,13 +17,6 @@ "vs/*": [ "./vs/*" ] - }, - "types": [ - "keytar", - "mocha", - "semver", - "sinon", - "winreg" - ] + } } } diff --git a/src/tsconfig.json b/src/tsconfig.json index 7fbb44c3ec5dc..b847912b35c17 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -11,6 +11,13 @@ "es5", "es2015.iterable", "webworker" + ], + "types": [ + "keytar", + "mocha", + "semver", + "sinon", + "winreg" ] }, "include": [ diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 32983503724dd..aa0af48b342b8 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -14,7 +14,7 @@ }, "include": [ "typings/require.d.ts", - "./typings/require-monaco.d.ts", + "typings/require-monaco.d.ts", "typings/thenable.d.ts", "typings/es6-promise.d.ts", "typings/lib.es2018.promise.d.ts", diff --git a/src/typings/applicationinsights-web.d.ts b/src/typings/applicationinsights-web.d.ts new file mode 100644 index 0000000000000..5af4903525c72 --- /dev/null +++ b/src/typings/applicationinsights-web.d.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module '@microsoft/applicationinsights-web' { + export interface IConfig { + instrumentationKey?: string; + endpointUrl?: string; + emitLineDelimitedJson?: boolean; + accountId?: string; + sessionRenewalMs?: number; + sessionExpirationMs?: number; + maxBatchSizeInBytes?: number; + maxBatchInterval?: number; + enableDebug?: boolean; + disableExceptionTracking?: boolean; + disableTelemetry?: boolean; + verboseLogging?: boolean; + diagnosticLogInterval?: number; + samplingPercentage?: number; + autoTrackPageVisitTime?: boolean; + disableAjaxTracking?: boolean; + overridePageViewDuration?: boolean; + maxAjaxCallsPerView?: number; + disableDataLossAnalysis?: boolean; + disableCorrelationHeaders?: boolean; + correlationHeaderExcludedDomains?: string[]; + disableFlushOnBeforeUnload?: boolean; + enableSessionStorageBuffer?: boolean; + isCookieUseDisabled?: boolean; + cookieDomain?: string; + isRetryDisabled?: boolean; + url?: string; + isStorageUseDisabled?: boolean; + isBeaconApiDisabled?: boolean; + sdkExtension?: string; + isBrowserLinkTrackingEnabled?: boolean; + appId?: string; + enableCorsCorrelation?: boolean; + } + + export interface ISnippet { + config: IConfig; + } + + export interface IEventTelemetry { + name: string; + properties?: { [key: string]: string }; + measurements?: { [key: string]: number }; + } + + export class ApplicationInsights { + constructor(config: ISnippet); + loadAppInsights(): void; + trackEvent(data: IEventTelemetry): void; + flush(): void; + } +} diff --git a/src/typings/xterm.d.ts b/src/typings/xterm.d.ts index 663b997022ba6..df7f20e233e68 100644 --- a/src/typings/xterm.d.ts +++ b/src/typings/xterm.d.ts @@ -207,47 +207,47 @@ declare module 'xterm' { */ export interface ITheme { /** The default foreground color */ - foreground?: string, + foreground?: string; /** The default background color */ - background?: string, + background?: string; /** The cursor color */ - cursor?: string, + cursor?: string; /** The accent color of the cursor (fg color for a block cursor) */ - cursorAccent?: string, + cursorAccent?: string; /** The selection background color (can be transparent) */ - selection?: string, + selection?: string; /** ANSI black (eg. `\x1b[30m`) */ - black?: string, + black?: string; /** ANSI red (eg. `\x1b[31m`) */ - red?: string, + red?: string; /** ANSI green (eg. `\x1b[32m`) */ - green?: string, + green?: string; /** ANSI yellow (eg. `\x1b[33m`) */ - yellow?: string, + yellow?: string; /** ANSI blue (eg. `\x1b[34m`) */ - blue?: string, + blue?: string; /** ANSI magenta (eg. `\x1b[35m`) */ - magenta?: string, + magenta?: string; /** ANSI cyan (eg. `\x1b[36m`) */ - cyan?: string, + cyan?: string; /** ANSI white (eg. `\x1b[37m`) */ - white?: string, + white?: string; /** ANSI bright black (eg. `\x1b[1;30m`) */ - brightBlack?: string, + brightBlack?: string; /** ANSI bright red (eg. `\x1b[1;31m`) */ - brightRed?: string, + brightRed?: string; /** ANSI bright green (eg. `\x1b[1;32m`) */ - brightGreen?: string, + brightGreen?: string; /** ANSI bright yellow (eg. `\x1b[1;33m`) */ - brightYellow?: string, + brightYellow?: string; /** ANSI bright blue (eg. `\x1b[1;34m`) */ - brightBlue?: string, + brightBlue?: string; /** ANSI bright magenta (eg. `\x1b[1;35m`) */ - brightMagenta?: string, + brightMagenta?: string; /** ANSI bright cyan (eg. `\x1b[1;36m`) */ - brightCyan?: string, + brightCyan?: string; /** ANSI bright white (eg. `\x1b[1;37m`) */ - brightWhite?: string + brightWhite?: string; } /** @@ -386,6 +386,12 @@ declare module 'xterm' { */ readonly markers: ReadonlyArray; + /** + * (EXPERIMENTAL) Get the parser interface to register + * custom escape sequence handlers. + */ + readonly parser: IParser; + /** * Natural language strings that can be localized. */ @@ -500,32 +506,6 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - /** - * (EXPERIMENTAL) Adds a handler for CSI escape sequences. - * @param flag The flag should be one-character string, which specifies the - * final character (e.g "m" for SGR) of the CSI sequence. - * @param callback The function to handle the escape sequence. The callback - * is called with the numerical params, as well as the special characters - * (e.g. "$" for DECSCPP). If the sequence has subparams the array will - * contain subarrays with their numercial values. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; - - /** - * (EXPERIMENTAL) Adds a handler for OSC escape sequences. - * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the escape sequence. The callback - * is called with OSC data string. Return true if the sequence was handled; - * false if we should try a previous handler (set by addOscHandler or - * setOscHandler). The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; - /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. @@ -689,6 +669,12 @@ declare module 'xterm' { */ writeUtf8(data: Uint8Array): void; + /** + * Writes text to the terminal, performing the necessary transformations for pasted text. + * @param data The text to write to the terminal. + */ + paste(data: string): void; + /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -804,18 +790,10 @@ declare module 'xterm' { /** * Perform a full reset (RIS, aka '\x1bc'). */ - reset(): void - - /** - * Applies an addon to the Terminal prototype, making it available to all - * newly created Terminals. - * @param addon The addon to apply. - * @deprecated Use the new loadAddon API/addon format. - */ - static applyAddon(addon: any): void; + reset(): void; /** - * (EXPERIMENTAL) Loads an addon into this instance of xterm.js. + * Loads an addon into this instance of xterm.js. * @param addon The addon to load. */ loadAddon(addon: ITerminalAddon): void; @@ -951,6 +929,119 @@ declare module 'xterm' { */ readonly width: number; } + + /** + * (EXPERIMENTAL) Data type to register a CSI, DCS or ESC callback in the parser + * in the form: + * ESC I..I F + * CSI Prefix P..P I..I F + * DCS Prefix P..P I..I F data_bytes ST + * + * with these rules/restrictions: + * - prefix can only be used with CSI and DCS + * - only one leading prefix byte is recognized by the parser + * before any other parameter bytes (P..P) + * - intermediate bytes are recognized up to 2 + * + * For custom sequences make sure to read ECMA-48 and the resources at + * vt100.net to not clash with existing sequences or reserved address space. + * General recommendations: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * thus parsers might get confused with more intermediates) + * - test against other common emulators to check whether they escape/ignore + * the sequence correctly + * + * Notes: OSC command registration is handled differently (see addOscHandler) + * APC, PM or SOS is currently not supported. + */ + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. + */ + final: string; + } + + /** + * (EXPERIMENTAL) Parser interface. + */ + export interface IParser { + /** + * Adds a handler for CSI escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. If the sequence has subparams the + * array will contain subarrays with their numercial values. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for DCS escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for ESC escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + + /** + * Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addOscHandler or setOscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + } } @@ -987,4 +1078,4 @@ declare module 'xterm' { interface Terminal { _core: TerminalCore; } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 717041747da9f..28fbfc82f45ec 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -15,7 +15,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import * as platform from 'vs/base/common/platform'; import { coalesce } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; +import { Schemas, RemoteAuthorities } from 'vs/base/common/network'; export function clearNode(node: HTMLElement): void { while (node.firstChild) { @@ -855,6 +855,7 @@ export const EventType = { KEY_UP: 'keyup', // HTML Document LOAD: 'load', + BEFORE_UNLOAD: 'beforeunload', UNLOAD: 'unload', ABORT: 'abort', ERROR: 'error', @@ -1185,22 +1186,24 @@ export function animate(fn: () => void): IDisposable { return toDisposable(() => stepDisposable.dispose()); } - - -const _location = URI.parse(window.location.href); +RemoteAuthorities.setPreferredWebSchema(/^https:/.test(window.location.href) ? 'https' : 'http'); export function asDomUri(uri: URI): URI { if (!uri) { return uri; } - if (!platform.isWeb) { - //todo@joh remove this once we have sw in electron going - return uri; - } if (Schemas.vscodeRemote === uri.scheme) { - // rewrite vscode-remote-uris to uris of the window location - // so that they can be intercepted by the service worker - return _location.with({ path: '/vscode-remote', query: JSON.stringify(uri) }); + return RemoteAuthorities.rewrite(uri.authority, uri.path); } return uri; } + +/** + * returns url('...') + */ +export function asCSSUrl(uri: URI): string { + if (!uri) { + return `url('')`; + } + return `url('${asDomUri(uri).toString(true).replace(/'/g, '%27')}')`; +} diff --git a/src/vs/base/browser/formattedTextRenderer.ts b/src/vs/base/browser/formattedTextRenderer.ts new file mode 100644 index 0000000000000..0e9b0c9571cd2 --- /dev/null +++ b/src/vs/base/browser/formattedTextRenderer.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export interface IContentActionHandler { + callback: (content: string, event?: IMouseEvent) => void; + readonly disposeables: DisposableStore; +} + +export interface FormattedTextRenderOptions { + readonly className?: string; + readonly inline?: boolean; + readonly actionHandler?: IContentActionHandler; +} + +export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement { + const element = createElement(options); + element.textContent = text; + return element; +} + +export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement { + const element = createElement(options); + _renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler); + return element; +} + +export function createElement(options: FormattedTextRenderOptions): HTMLElement { + const tagName = options.inline ? 'span' : 'div'; + const element = document.createElement(tagName); + if (options.className) { + element.className = options.className; + } + return element; +} + +class StringStream { + private source: string; + private index: number; + + constructor(source: string) { + this.source = source; + this.index = 0; + } + + public eos(): boolean { + return this.index >= this.source.length; + } + + public next(): string { + const next = this.peek(); + this.advance(); + return next; + } + + public peek(): string { + return this.source[this.index]; + } + + public advance(): void { + this.index++; + } +} + +const enum FormatType { + Invalid, + Root, + Text, + Bold, + Italics, + Action, + ActionClose, + NewLine +} + +interface IFormatParseTree { + type: FormatType; + content?: string; + index?: number; + children?: IFormatParseTree[]; +} + +function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) { + let child: Node | undefined; + + if (treeNode.type === FormatType.Text) { + child = document.createTextNode(treeNode.content || ''); + } else if (treeNode.type === FormatType.Bold) { + child = document.createElement('b'); + } else if (treeNode.type === FormatType.Italics) { + child = document.createElement('i'); + } else if (treeNode.type === FormatType.Action && actionHandler) { + const a = document.createElement('a'); + a.href = '#'; + actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => { + actionHandler.callback(String(treeNode.index), event); + })); + + child = a; + } else if (treeNode.type === FormatType.NewLine) { + child = document.createElement('br'); + } else if (treeNode.type === FormatType.Root) { + child = element; + } + + if (child && element !== child) { + element.appendChild(child); + } + + if (child && Array.isArray(treeNode.children)) { + treeNode.children.forEach((nodeChild) => { + _renderFormattedText(child!, nodeChild, actionHandler); + }); + } +} + +function parseFormattedText(content: string): IFormatParseTree { + + const root: IFormatParseTree = { + type: FormatType.Root, + children: [] + }; + + let actionViewItemIndex = 0; + let current = root; + const stack: IFormatParseTree[] = []; + const stream = new StringStream(content); + + while (!stream.eos()) { + let next = stream.next(); + + const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid); + if (isEscapedFormatType) { + next = stream.next(); // unread the backslash if it escapes a format tag type + } + + if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) { + stream.advance(); + + if (current.type === FormatType.Text) { + current = stack.pop()!; + } + + const type = formatTagType(next); + if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) { + current = stack.pop()!; + } else { + const newCurrent: IFormatParseTree = { + type: type, + children: [] + }; + + if (type === FormatType.Action) { + newCurrent.index = actionViewItemIndex; + actionViewItemIndex++; + } + + current.children!.push(newCurrent); + stack.push(current); + current = newCurrent; + } + } else if (next === '\n') { + if (current.type === FormatType.Text) { + current = stack.pop()!; + } + + current.children!.push({ + type: FormatType.NewLine + }); + + } else { + if (current.type !== FormatType.Text) { + const textCurrent: IFormatParseTree = { + type: FormatType.Text, + content: next + }; + current.children!.push(textCurrent); + stack.push(current); + current = textCurrent; + + } else { + current.content += next; + } + } + } + + if (current.type === FormatType.Text) { + current = stack.pop()!; + } + + if (stack.length) { + // incorrectly formatted string literal + } + + return root; +} + +function isFormatTag(char: string): boolean { + return formatTagType(char) !== FormatType.Invalid; +} + +function formatTagType(char: string): FormatType { + switch (char) { + case '*': + return FormatType.Bold; + case '_': + return FormatType.Italics; + case '[': + return FormatType.Action; + case ']': + return FormatType.ActionClose; + default: + return FormatType.Invalid; + } +} diff --git a/src/vs/base/browser/htmlContentRenderer.ts b/src/vs/base/browser/htmlContentRenderer.ts deleted file mode 100644 index 4506912e178e9..0000000000000 --- a/src/vs/base/browser/htmlContentRenderer.ts +++ /dev/null @@ -1,394 +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 DOM from 'vs/base/browser/dom'; -import { defaultGenerator } from 'vs/base/common/idGenerator'; -import { escape } from 'vs/base/common/strings'; -import { removeMarkdownEscapes, IMarkdownString, parseHrefAndDimensions } from 'vs/base/common/htmlContent'; -import * as marked from 'vs/base/common/marked/marked'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { URI } from 'vs/base/common/uri'; -import { parse } from 'vs/base/common/marshalling'; -import { cloneAndChange } from 'vs/base/common/objects'; - -export interface IContentActionHandler { - callback: (content: string, event?: IMouseEvent) => void; - readonly disposeables: DisposableStore; -} - -export interface RenderOptions { - className?: string; - inline?: boolean; - actionHandler?: IContentActionHandler; - codeBlockRenderer?: (modeId: string, value: string) => Promise; - codeBlockRenderCallback?: () => void; -} - -function createElement(options: RenderOptions): HTMLElement { - const tagName = options.inline ? 'span' : 'div'; - const element = document.createElement(tagName); - if (options.className) { - element.className = options.className; - } - return element; -} - -export function renderText(text: string, options: RenderOptions = {}): HTMLElement { - const element = createElement(options); - element.textContent = text; - return element; -} - -export function renderFormattedText(formattedText: string, options: RenderOptions = {}): HTMLElement { - const element = createElement(options); - _renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler); - return element; -} - -/** - * Create html nodes for the given content element. - */ -export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement { - const element = createElement(options); - - const _uriMassage = function (part: string): string { - let data: any; - try { - data = parse(decodeURIComponent(part)); - } catch (e) { - // ignore - } - if (!data) { - return part; - } - data = cloneAndChange(data, value => { - if (markdown.uris && markdown.uris[value]) { - return URI.revive(markdown.uris[value]); - } else { - return undefined; - } - }); - return encodeURIComponent(JSON.stringify(data)); - }; - - const _href = function (href: string, isDomUri: boolean): string { - const data = markdown.uris && markdown.uris[href]; - if (!data) { - return href; - } - let uri = URI.revive(data); - if (isDomUri) { - uri = DOM.asDomUri(uri); - } - if (uri.query) { - uri = uri.with({ query: _uriMassage(uri.query) }); - } - if (data) { - href = uri.toString(true); - } - return href; - }; - - // signal to code-block render that the - // element has been created - let signalInnerHTML: () => void; - const withInnerHTML = new Promise(c => signalInnerHTML = c); - - const renderer = new marked.Renderer(); - renderer.image = (href: string, title: string, text: string) => { - let dimensions: string[] = []; - let attributes: string[] = []; - if (href) { - ({ href, dimensions } = parseHrefAndDimensions(href)); - href = _href(href, true); - attributes.push(`src="${href}"`); - } - if (text) { - attributes.push(`alt="${text}"`); - } - if (title) { - attributes.push(`title="${title}"`); - } - if (dimensions.length) { - attributes = attributes.concat(dimensions); - } - return ''; - }; - renderer.link = (href, title, text): string => { - // Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829 - if (href === text) { // raw link case - text = removeMarkdownEscapes(text); - } - href = _href(href, false); - title = removeMarkdownEscapes(title); - href = removeMarkdownEscapes(href); - if ( - !href - || href.match(/^data:|javascript:/i) - || (href.match(/^command:/i) && !markdown.isTrusted) - || href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i) - ) { - // drop the link - return text; - - } else { - // HTML Encode href - href = href.replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - return `${text}`; - } - }; - renderer.paragraph = (text): string => { - return `

${text}

`; - }; - - if (options.codeBlockRenderer) { - renderer.code = (code, lang) => { - const value = options.codeBlockRenderer!(lang, code); - // when code-block rendering is async we return sync - // but update the node with the real result later. - const id = defaultGenerator.nextId(); - const promise = Promise.all([value, withInnerHTML]).then(values => { - const strValue = values[0]; - const span = element.querySelector(`div[data-code="${id}"]`); - if (span) { - span.innerHTML = strValue; - } - }).catch(err => { - // ignore - }); - - if (options.codeBlockRenderCallback) { - promise.then(options.codeBlockRenderCallback); - } - - return `
${escape(code)}
`; - }; - } - - if (options.actionHandler) { - options.actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => { - let target: HTMLElement | null = event.target; - if (target.tagName !== 'A') { - target = target.parentElement; - if (!target || target.tagName !== 'A') { - return; - } - } - try { - const href = target.dataset['href']; - if (href) { - options.actionHandler!.callback(href, event); - } - } catch (err) { - onUnexpectedError(err); - } finally { - event.preventDefault(); - } - })); - } - - const markedOptions: marked.MarkedOptions = { - sanitize: true, - renderer - }; - - element.innerHTML = marked.parse(markdown.value, markedOptions); - signalInnerHTML!(); - - return element; -} - -// --- formatted string parsing - -class StringStream { - private source: string; - private index: number; - - constructor(source: string) { - this.source = source; - this.index = 0; - } - - public eos(): boolean { - return this.index >= this.source.length; - } - - public next(): string { - const next = this.peek(); - this.advance(); - return next; - } - - public peek(): string { - return this.source[this.index]; - } - - public advance(): void { - this.index++; - } -} - -const enum FormatType { - Invalid, - Root, - Text, - Bold, - Italics, - Action, - ActionClose, - NewLine -} - -interface IFormatParseTree { - type: FormatType; - content?: string; - index?: number; - children?: IFormatParseTree[]; -} - -function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) { - let child: Node | undefined; - - if (treeNode.type === FormatType.Text) { - child = document.createTextNode(treeNode.content || ''); - } - else if (treeNode.type === FormatType.Bold) { - child = document.createElement('b'); - } - else if (treeNode.type === FormatType.Italics) { - child = document.createElement('i'); - } - else if (treeNode.type === FormatType.Action && actionHandler) { - const a = document.createElement('a'); - a.href = '#'; - actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => { - actionHandler.callback(String(treeNode.index), event); - })); - - child = a; - } - else if (treeNode.type === FormatType.NewLine) { - child = document.createElement('br'); - } - else if (treeNode.type === FormatType.Root) { - child = element; - } - - if (child && element !== child) { - element.appendChild(child); - } - - if (child && Array.isArray(treeNode.children)) { - treeNode.children.forEach((nodeChild) => { - _renderFormattedText(child!, nodeChild, actionHandler); - }); - } -} - -function parseFormattedText(content: string): IFormatParseTree { - - const root: IFormatParseTree = { - type: FormatType.Root, - children: [] - }; - - let actionViewItemIndex = 0; - let current = root; - const stack: IFormatParseTree[] = []; - const stream = new StringStream(content); - - while (!stream.eos()) { - let next = stream.next(); - - const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid); - if (isEscapedFormatType) { - next = stream.next(); // unread the backslash if it escapes a format tag type - } - - if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) { - stream.advance(); - - if (current.type === FormatType.Text) { - current = stack.pop()!; - } - - const type = formatTagType(next); - if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) { - current = stack.pop()!; - } else { - const newCurrent: IFormatParseTree = { - type: type, - children: [] - }; - - if (type === FormatType.Action) { - newCurrent.index = actionViewItemIndex; - actionViewItemIndex++; - } - - current.children!.push(newCurrent); - stack.push(current); - current = newCurrent; - } - } else if (next === '\n') { - if (current.type === FormatType.Text) { - current = stack.pop()!; - } - - current.children!.push({ - type: FormatType.NewLine - }); - - } else { - if (current.type !== FormatType.Text) { - const textCurrent: IFormatParseTree = { - type: FormatType.Text, - content: next - }; - current.children!.push(textCurrent); - stack.push(current); - current = textCurrent; - - } else { - current.content += next; - } - } - } - - if (current.type === FormatType.Text) { - current = stack.pop()!; - } - - if (stack.length) { - // incorrectly formatted string literal - } - - return root; -} - -function isFormatTag(char: string): boolean { - return formatTagType(char) !== FormatType.Invalid; -} - -function formatTagType(char: string): FormatType { - switch (char) { - case '*': - return FormatType.Bold; - case '_': - return FormatType.Italics; - case '[': - return FormatType.Action; - case ']': - return FormatType.ActionClose; - default: - return FormatType.Invalid; - } -} diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts new file mode 100644 index 0000000000000..fa6956fdac07d --- /dev/null +++ b/src/vs/base/browser/markdownRenderer.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent'; +import { defaultGenerator } from 'vs/base/common/idGenerator'; +import * as marked from 'vs/base/common/marked/marked'; +import * as insane from 'vs/base/common/insane/insane'; +import { parse } from 'vs/base/common/marshalling'; +import { cloneAndChange } from 'vs/base/common/objects'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; + +export interface MarkdownRenderOptions extends FormattedTextRenderOptions { + codeBlockRenderer?: (modeId: string, value: string) => Promise; + codeBlockRenderCallback?: () => void; +} + +/** + * Create html nodes for the given content element. + */ +export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}): HTMLElement { + const element = createElement(options); + + const _uriMassage = function (part: string): string { + let data: any; + try { + data = parse(decodeURIComponent(part)); + } catch (e) { + // ignore + } + if (!data) { + return part; + } + data = cloneAndChange(data, value => { + if (markdown.uris && markdown.uris[value]) { + return URI.revive(markdown.uris[value]); + } else { + return undefined; + } + }); + return encodeURIComponent(JSON.stringify(data)); + }; + + const _href = function (href: string, isDomUri: boolean): string { + const data = markdown.uris && markdown.uris[href]; + if (!data) { + return href; + } + let uri = URI.revive(data); + if (isDomUri) { + uri = DOM.asDomUri(uri); + } + if (uri.query) { + uri = uri.with({ query: _uriMassage(uri.query) }); + } + if (data) { + href = uri.toString(true); + } + return href; + }; + + // signal to code-block render that the + // element has been created + let signalInnerHTML: () => void; + const withInnerHTML = new Promise(c => signalInnerHTML = c); + + const renderer = new marked.Renderer(); + renderer.image = (href: string, title: string, text: string) => { + let dimensions: string[] = []; + let attributes: string[] = []; + if (href) { + ({ href, dimensions } = parseHrefAndDimensions(href)); + href = _href(href, true); + attributes.push(`src="${href}"`); + } + if (text) { + attributes.push(`alt="${text}"`); + } + if (title) { + attributes.push(`title="${title}"`); + } + if (dimensions.length) { + attributes = attributes.concat(dimensions); + } + return ''; + }; + renderer.link = (href, title, text): string => { + // Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829 + if (href === text) { // raw link case + text = removeMarkdownEscapes(text); + } + href = _href(href, false); + title = removeMarkdownEscapes(title); + href = removeMarkdownEscapes(href); + if ( + !href + || href.match(/^data:|javascript:/i) + || (href.match(/^command:/i) && !markdown.isTrusted) + || href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i) + ) { + // drop the link + return text; + + } else { + // HTML Encode href + href = href.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + return `${text}`; + } + }; + renderer.paragraph = (text): string => { + return `

${text}

`; + }; + + if (options.codeBlockRenderer) { + renderer.code = (code, lang) => { + const value = options.codeBlockRenderer!(lang, code); + // when code-block rendering is async we return sync + // but update the node with the real result later. + const id = defaultGenerator.nextId(); + const promise = Promise.all([value, withInnerHTML]).then(values => { + const strValue = values[0]; + const span = element.querySelector(`div[data-code="${id}"]`); + if (span) { + span.innerHTML = strValue; + } + }).catch(err => { + // ignore + }); + + if (options.codeBlockRenderCallback) { + promise.then(options.codeBlockRenderCallback); + } + + return `
${escape(code)}
`; + }; + } + + const actionHandler = options.actionHandler; + if (actionHandler) { + actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => { + let target: HTMLElement | null = event.target; + if (target.tagName !== 'A') { + target = target.parentElement; + if (!target || target.tagName !== 'A') { + return; + } + } + try { + const href = target.dataset['href']; + if (href) { + actionHandler.callback(href, event); + } + } catch (err) { + onUnexpectedError(err); + } finally { + event.preventDefault(); + } + })); + } + + const markedOptions: marked.MarkedOptions = { + sanitize: true, + renderer + }; + + const allowedSchemes = ['http', 'https', 'mailto']; + if (markdown.isTrusted) { + allowedSchemes.push('command'); + } + + const renderedMarkdown = marked.parse(markdown.value, markedOptions); + element.innerHTML = insane(renderedMarkdown, { + allowedSchemes, + allowedAttributes: { + 'a': ['href', 'name', 'target', 'data-href'], + 'iframe': ['allowfullscreen', 'frameborder', 'src'], + 'img': ['src', 'title', 'alt', 'width', 'height'], + 'div': ['class', 'data-code'] + } + }); + + signalInnerHTML!(); + + return element; +} diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 2a2964beeef82..fcc27e402a771 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -24,6 +24,8 @@ export interface IFindInputOptions extends IFindInputStyles { readonly validation?: IInputValidator; readonly label: string; readonly flexibleHeight?: boolean; + readonly flexibleWidth?: boolean; + readonly flexibleMaxHeight?: number; readonly appendCaseSensitiveLabel?: string; readonly appendWholeWordsLabel?: string; @@ -119,6 +121,8 @@ export class FindInput extends Widget { const appendRegexLabel = options.appendRegexLabel || ''; const history = options.history || []; const flexibleHeight = !!options.flexibleHeight; + const flexibleWidth = !!options.flexibleWidth; + const flexibleMaxHeight = options.flexibleMaxHeight; this.domNode = document.createElement('div'); dom.addClass(this.domNode, 'monaco-findInput'); @@ -142,7 +146,9 @@ export class FindInput extends Widget { inputValidationErrorForeground: this.inputValidationErrorForeground, inputValidationErrorBorder: this.inputValidationErrorBorder, history, - flexibleHeight + flexibleHeight, + flexibleWidth, + flexibleMaxHeight })); this.regex = this._register(new RegexCheckbox({ @@ -194,11 +200,7 @@ export class FindInput extends Widget { })); if (this._showOptionButtons) { - const paddingRight = (this.caseSensitive.width() + this.wholeWords.width() + this.regex.width()) + 'px'; - this.inputBox.inputElement.style.paddingRight = paddingRight; - if (this.inputBox.mirrorElement) { - this.inputBox.mirrorElement.style.paddingRight = paddingRight; - } + this.inputBox.paddingRight = this.caseSensitive.width() + this.wholeWords.width() + this.regex.width(); } // Arrow-Key support to navigate between options diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts new file mode 100644 index 0000000000000..d597fb8bd49c7 --- /dev/null +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -0,0 +1,373 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./findInput'; + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IMessage as InputBoxMessage, IInputValidator, IInputBoxStyles, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Color } from 'vs/base/common/color'; +import { ICheckboxStyles, Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { IFindInputCheckboxOpts } from 'vs/base/browser/ui/findinput/findInputCheckboxes'; + +export interface IReplaceInputOptions extends IReplaceInputStyles { + readonly placeholder?: string; + readonly width?: number; + readonly validation?: IInputValidator; + readonly label: string; + readonly flexibleHeight?: boolean; + readonly flexibleWidth?: boolean; + readonly flexibleMaxHeight?: number; + + readonly history?: string[]; +} + +export interface IReplaceInputStyles extends IInputBoxStyles { + inputActiveOptionBorder?: Color; +} + +const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input"); +const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case"); + +export class PreserveCaseCheckbox extends Checkbox { + constructor(opts: IFindInputCheckboxOpts) { + super({ + // TODO: does this need its own icon? + actionClassName: 'monaco-case-sensitive', + title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle, + isChecked: opts.isChecked, + inputActiveOptionBorder: opts.inputActiveOptionBorder + }); + } +} + +export class ReplaceInput extends Widget { + + static readonly OPTION_CHANGE: string = 'optionChange'; + + private contextViewProvider: IContextViewProvider | undefined; + private placeholder: string; + private validation?: IInputValidator; + private label: string; + private fixFocusOnOptionClickEnabled = true; + + private inputActiveOptionBorder?: Color; + private inputBackground?: Color; + private inputForeground?: Color; + private inputBorder?: Color; + + private inputValidationInfoBorder?: Color; + private inputValidationInfoBackground?: Color; + private inputValidationInfoForeground?: Color; + private inputValidationWarningBorder?: Color; + private inputValidationWarningBackground?: Color; + private inputValidationWarningForeground?: Color; + private inputValidationErrorBorder?: Color; + private inputValidationErrorBackground?: Color; + private inputValidationErrorForeground?: Color; + + private preserveCase: PreserveCaseCheckbox; + private cachedOptionsWidth: number = 0; + public domNode: HTMLElement; + public inputBox: HistoryInputBox; + + private readonly _onDidOptionChange = this._register(new Emitter()); + public readonly onDidOptionChange: Event = this._onDidOptionChange.event; + + private readonly _onKeyDown = this._register(new Emitter()); + public readonly onKeyDown: Event = this._onKeyDown.event; + + private readonly _onMouseDown = this._register(new Emitter()); + public readonly onMouseDown: Event = this._onMouseDown.event; + + private readonly _onInput = this._register(new Emitter()); + public readonly onInput: Event = this._onInput.event; + + private readonly _onKeyUp = this._register(new Emitter()); + public readonly onKeyUp: Event = this._onKeyUp.event; + + private _onPreserveCaseKeyDown = this._register(new Emitter()); + public readonly onPreserveCaseKeyDown: Event = this._onPreserveCaseKeyDown.event; + + constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, private readonly _showOptionButtons: boolean, options: IReplaceInputOptions) { + super(); + this.contextViewProvider = contextViewProvider; + this.placeholder = options.placeholder || ''; + this.validation = options.validation; + this.label = options.label || NLS_DEFAULT_LABEL; + + this.inputActiveOptionBorder = options.inputActiveOptionBorder; + this.inputBackground = options.inputBackground; + this.inputForeground = options.inputForeground; + this.inputBorder = options.inputBorder; + + this.inputValidationInfoBorder = options.inputValidationInfoBorder; + this.inputValidationInfoBackground = options.inputValidationInfoBackground; + this.inputValidationInfoForeground = options.inputValidationInfoForeground; + this.inputValidationWarningBorder = options.inputValidationWarningBorder; + this.inputValidationWarningBackground = options.inputValidationWarningBackground; + this.inputValidationWarningForeground = options.inputValidationWarningForeground; + this.inputValidationErrorBorder = options.inputValidationErrorBorder; + this.inputValidationErrorBackground = options.inputValidationErrorBackground; + this.inputValidationErrorForeground = options.inputValidationErrorForeground; + + const flexibleHeight = !!options.flexibleHeight; + const flexibleWidth = !!options.flexibleWidth; + const flexibleMaxHeight = options.flexibleMaxHeight; + + this.buildDomNode(options.history || [], flexibleHeight, flexibleWidth, flexibleMaxHeight); + + if (parent) { + parent.appendChild(this.domNode); + } + + this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e)); + this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e)); + this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire()); + this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e)); + } + + public enable(): void { + dom.removeClass(this.domNode, 'disabled'); + this.inputBox.enable(); + this.preserveCase.enable(); + } + + public disable(): void { + dom.addClass(this.domNode, 'disabled'); + this.inputBox.disable(); + this.preserveCase.disable(); + } + + public setFocusInputOnOptionClick(value: boolean): void { + this.fixFocusOnOptionClickEnabled = value; + } + + public setEnabled(enabled: boolean): void { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + } + + public clear(): void { + this.clearValidation(); + this.setValue(''); + this.focus(); + } + + public getValue(): string { + return this.inputBox.value; + } + + public setValue(value: string): void { + if (this.inputBox.value !== value) { + this.inputBox.value = value; + } + } + + public onSearchSubmit(): void { + this.inputBox.addToHistory(); + } + + public style(styles: IReplaceInputStyles): void { + this.inputActiveOptionBorder = styles.inputActiveOptionBorder; + this.inputBackground = styles.inputBackground; + this.inputForeground = styles.inputForeground; + this.inputBorder = styles.inputBorder; + + this.inputValidationInfoBackground = styles.inputValidationInfoBackground; + this.inputValidationInfoForeground = styles.inputValidationInfoForeground; + this.inputValidationInfoBorder = styles.inputValidationInfoBorder; + this.inputValidationWarningBackground = styles.inputValidationWarningBackground; + this.inputValidationWarningForeground = styles.inputValidationWarningForeground; + this.inputValidationWarningBorder = styles.inputValidationWarningBorder; + this.inputValidationErrorBackground = styles.inputValidationErrorBackground; + this.inputValidationErrorForeground = styles.inputValidationErrorForeground; + this.inputValidationErrorBorder = styles.inputValidationErrorBorder; + + this.applyStyles(); + } + + protected applyStyles(): void { + if (this.domNode) { + const checkBoxStyles: ICheckboxStyles = { + inputActiveOptionBorder: this.inputActiveOptionBorder, + }; + this.preserveCase.style(checkBoxStyles); + + const inputBoxStyles: IInputBoxStyles = { + inputBackground: this.inputBackground, + inputForeground: this.inputForeground, + inputBorder: this.inputBorder, + inputValidationInfoBackground: this.inputValidationInfoBackground, + inputValidationInfoForeground: this.inputValidationInfoForeground, + inputValidationInfoBorder: this.inputValidationInfoBorder, + inputValidationWarningBackground: this.inputValidationWarningBackground, + inputValidationWarningForeground: this.inputValidationWarningForeground, + inputValidationWarningBorder: this.inputValidationWarningBorder, + inputValidationErrorBackground: this.inputValidationErrorBackground, + inputValidationErrorForeground: this.inputValidationErrorForeground, + inputValidationErrorBorder: this.inputValidationErrorBorder + }; + this.inputBox.style(inputBoxStyles); + } + } + + public select(): void { + this.inputBox.select(); + } + + public focus(): void { + this.inputBox.focus(); + } + + public getPreserveCase(): boolean { + return this.preserveCase.checked; + } + + public setPreserveCase(value: boolean): void { + this.preserveCase.checked = value; + } + + public focusOnPreserve(): void { + this.preserveCase.focus(); + } + + private _lastHighlightFindOptions: number = 0; + public highlightFindOptions(): void { + dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions; + dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + } + + private buildDomNode(history: string[], flexibleHeight: boolean, flexibleWidth: boolean, flexibleMaxHeight: number | undefined): void { + this.domNode = document.createElement('div'); + dom.addClass(this.domNode, 'monaco-findInput'); + + this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, { + ariaLabel: this.label || '', + placeholder: this.placeholder || '', + validationOptions: { + validation: this.validation + }, + inputBackground: this.inputBackground, + inputForeground: this.inputForeground, + inputBorder: this.inputBorder, + inputValidationInfoBackground: this.inputValidationInfoBackground, + inputValidationInfoForeground: this.inputValidationInfoForeground, + inputValidationInfoBorder: this.inputValidationInfoBorder, + inputValidationWarningBackground: this.inputValidationWarningBackground, + inputValidationWarningForeground: this.inputValidationWarningForeground, + inputValidationWarningBorder: this.inputValidationWarningBorder, + inputValidationErrorBackground: this.inputValidationErrorBackground, + inputValidationErrorForeground: this.inputValidationErrorForeground, + inputValidationErrorBorder: this.inputValidationErrorBorder, + history, + flexibleHeight, + flexibleWidth, + flexibleMaxHeight + })); + + this.preserveCase = this._register(new PreserveCaseCheckbox({ + appendTitle: '', + isChecked: false, + inputActiveOptionBorder: this.inputActiveOptionBorder + })); + this._register(this.preserveCase.onChange(viaKeyboard => { + this._onDidOptionChange.fire(viaKeyboard); + if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) { + this.inputBox.focus(); + } + this.validate(); + })); + this._register(this.preserveCase.onKeyDown(e => { + this._onPreserveCaseKeyDown.fire(e); + })); + + if (this._showOptionButtons) { + this.cachedOptionsWidth = this.preserveCase.width(); + } else { + this.cachedOptionsWidth = 0; + } + + // Arrow-Key support to navigate between options + let indexes = [this.preserveCase.domNode]; + this.onkeydown(this.domNode, (event: IKeyboardEvent) => { + if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) { + let index = indexes.indexOf(document.activeElement); + if (index >= 0) { + let newIndex: number = -1; + if (event.equals(KeyCode.RightArrow)) { + newIndex = (index + 1) % indexes.length; + } else if (event.equals(KeyCode.LeftArrow)) { + if (index === 0) { + newIndex = indexes.length - 1; + } else { + newIndex = index - 1; + } + } + + if (event.equals(KeyCode.Escape)) { + indexes[index].blur(); + } else if (newIndex >= 0) { + indexes[newIndex].focus(); + } + + dom.EventHelper.stop(event, true); + } + } + }); + + + let controls = document.createElement('div'); + controls.className = 'controls'; + controls.style.display = this._showOptionButtons ? 'block' : 'none'; + controls.appendChild(this.preserveCase.domNode); + + this.domNode.appendChild(controls); + } + + public validate(): void { + if (this.inputBox) { + this.inputBox.validate(); + } + } + + public showMessage(message: InputBoxMessage): void { + if (this.inputBox) { + this.inputBox.showMessage(message); + } + } + + public clearMessage(): void { + if (this.inputBox) { + this.inputBox.hideMessage(); + } + } + + private clearValidation(): void { + if (this.inputBox) { + this.inputBox.hideMessage(); + } + } + + public set width(newWidth: number) { + this.inputBox.paddingRight = this.cachedOptionsWidth; + this.inputBox.width = newWidth; + this.domNode.style.width = newWidth + 'px'; + } + + public dispose(): void { + super.dispose(); + } +} diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index a35f9b2cdf430..4586d752d0b27 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -7,7 +7,7 @@ import 'vs/css!./gridview'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { Disposable } from 'vs/base/common/lifecycle'; import { tail2 as tail, equals } from 'vs/base/common/arrays'; -import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, ILayoutController, LayoutController } from './gridview'; +import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, LayoutController, IGridViewOptions } from './gridview'; import { Event } from 'vs/base/common/event'; import { InvisibleSizing } from 'vs/base/browser/ui/splitview/splitview'; @@ -193,11 +193,8 @@ export namespace Sizing { export interface IGridStyles extends IGridViewStyles { } -export interface IGridOptions { - readonly styles?: IGridStyles; - readonly proportionalLayout?: boolean; +export interface IGridOptions extends IGridViewOptions { readonly firstViewVisibleCachedSize?: number; - readonly layoutController?: ILayoutController; } export class Grid extends Disposable { diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 5cefe23c54952..934601cf17c02 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -76,6 +76,11 @@ export class LayoutController implements ILayoutController { constructor(public isLayoutEnabled: boolean) { } } +export class MultiplexLayoutController implements ILayoutController { + get isLayoutEnabled(): boolean { return this.layoutControllers.every(l => l.isLayoutEnabled); } + constructor(private layoutControllers: ILayoutController[]) { } +} + export interface IGridViewOptions { readonly styles?: IGridViewStyles; readonly proportionalLayout?: boolean; // default true @@ -170,6 +175,7 @@ class BranchNode implements ISplitView, IDisposable { constructor( readonly orientation: Orientation, + readonly layoutController: ILayoutController, styles: IGridViewStyles, readonly proportionalLayout: boolean, size: number = 0, @@ -181,7 +187,7 @@ class BranchNode implements ISplitView, IDisposable { this.element = $('.monaco-grid-branch-node'); this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout }); - this.splitview.layout(size); + this.splitview.layout(size, orthogonalSize); const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]); this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset); @@ -198,12 +204,20 @@ class BranchNode implements ISplitView, IDisposable { } } - layout(size: number): void { - this._orthogonalSize = size; + layout(size: number, orthogonalSize: number | undefined): void { + if (!this.layoutController.isLayoutEnabled) { + return; + } - for (const child of this.children) { - child.orthogonalLayout(size); + if (typeof orthogonalSize !== 'number') { + throw new Error('Invalid state'); } + + // branch nodes should flip the normal/orthogonal directions + this._size = orthogonalSize; + this._orthogonalSize = size; + + this.splitview.layout(orthogonalSize, size); } setVisible(visible: boolean): void { @@ -212,11 +226,6 @@ class BranchNode implements ISplitView, IDisposable { } } - orthogonalLayout(size: number): void { - this._size = size; - this.splitview.layout(size); - } - addChild(node: Node, size: number | Sizing, index: number): void { if (index < 0 || index > this.children.length) { throw new Error('Invalid index'); @@ -347,6 +356,10 @@ class BranchNode implements ISplitView, IDisposable { throw new Error('Invalid index'); } + if (this.splitview.isViewVisible(index) === visible) { + return; + } + this.splitview.setViewVisible(index, visible); this._onDidChange.fire(undefined); } @@ -539,12 +552,18 @@ class LeafNode implements ISplitView, IDisposable { // noop } - layout(size: number): void { - this._size = size; + layout(size: number, orthogonalSize: number | undefined): void { + if (!this.layoutController.isLayoutEnabled) { + return; + } - if (this.layoutController.isLayoutEnabled) { - this.view.layout(this.width, this.height, orthogonal(this.orientation)); + if (typeof orthogonalSize !== 'number') { + throw new Error('Invalid state'); } + + this._size = size; + this._orthogonalSize = orthogonalSize; + this.view.layout(this.width, this.height, orthogonal(this.orientation)); } setVisible(visible: boolean): void { @@ -553,14 +572,6 @@ class LeafNode implements ISplitView, IDisposable { } } - orthogonalLayout(size: number): void { - this._orthogonalSize = size; - - if (this.layoutController.isLayoutEnabled) { - this.view.layout(this.width, this.height, orthogonal(this.orientation)); - } - } - dispose(): void { } } @@ -568,7 +579,7 @@ type Node = BranchNode | LeafNode; function flipNode(node: T, size: number, orthogonalSize: number): T { if (node instanceof BranchNode) { - const result = new BranchNode(orthogonal(node.orientation), node.styles, node.proportionalLayout, size, orthogonalSize); + const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.proportionalLayout, size, orthogonalSize); let totalSize = 0; @@ -589,7 +600,7 @@ function flipNode(node: T, size: number, orthogonalSize: number) return result as T; } else { - return new LeafNode((node as LeafNode).view, orthogonal(node.orientation), (node as LeafNode).layoutController, orthogonalSize) as T; + return new LeafNode((node as LeafNode).view, orthogonal(node.orientation), node.layoutController, orthogonalSize) as T; } } @@ -634,8 +645,7 @@ export class GridView implements IDisposable { const { size, orthogonalSize } = this._root; this.root = flipNode(this._root, orthogonalSize, size); - this.root.layout(size); - this.root.orthogonalLayout(orthogonalSize); + this.root.layout(size, orthogonalSize); } get width(): number { return this.root.width; } @@ -649,14 +659,25 @@ export class GridView implements IDisposable { private _onDidChange = new Relay(); readonly onDidChange = this._onDidChange.event; + /** + * The first layout controller makes sure layout only propagates + * to the views after the very first call to gridview.layout() + */ + private firstLayoutController: LayoutController; private layoutController: LayoutController; constructor(options: IGridViewOptions = {}) { this.element = $('.monaco-grid-view'); this.styles = options.styles || defaultStyles; this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true; - this.root = new BranchNode(Orientation.VERTICAL, this.styles, this.proportionalLayout); - this.layoutController = options.layoutController || new LayoutController(true); + + this.firstLayoutController = new LayoutController(false); + this.layoutController = new MultiplexLayoutController([ + this.firstLayoutController, + ...(options.layoutController ? [options.layoutController] : []) + ]); + + this.root = new BranchNode(Orientation.VERTICAL, this.layoutController, this.styles, this.proportionalLayout); } style(styles: IGridViewStyles): void { @@ -665,9 +686,10 @@ export class GridView implements IDisposable { } layout(width: number, height: number): void { + this.firstLayoutController.isLayoutEnabled = true; + const [size, orthogonalSize] = this.root.orientation === Orientation.HORIZONTAL ? [height, width] : [width, height]; - this.root.layout(size); - this.root.orthogonalLayout(orthogonalSize); + this.root.layout(size, orthogonalSize); } addView(view: IView, size: number | Sizing, location: number[]): void { @@ -694,9 +716,8 @@ export class GridView implements IDisposable { grandParent.removeChild(parentIndex); - const newParent = new BranchNode(parent.orientation, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize); + const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize); grandParent.addChild(newParent, parent.size, parentIndex); - newParent.orthogonalLayout(parent.orthogonalSize); const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size); newParent.addChild(newSibling, newSiblingSize, 0); @@ -827,9 +848,6 @@ export class GridView implements IDisposable { fromParent.addChild(toNode, fromSize, fromIndex); toParent.addChild(fromNode, toSize, toIndex); - - fromParent.layout(fromParent.orthogonalSize); - toParent.layout(toParent.orthogonalSize); } } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index dc1240dfbc57a..d13e1f2e53595 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -60,6 +60,8 @@ display: block; -ms-overflow-style: none; /* IE 10+ */ overflow: -moz-scrollbars-none; /* Firefox */ + scrollbar-width: none; /* Firefox ^64 */ + outline: none; } .monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar { diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 59d0c9900b9e3..430f250ce386d 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -8,7 +8,8 @@ import 'vs/css!./inputBox'; import * as nls from 'vs/nls'; import * as Bal from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; -import { RenderOptions, renderFormattedText, renderText } from 'vs/base/browser/htmlContentRenderer'; +import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; +import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IAction } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -31,6 +32,7 @@ export interface IInputOptions extends IInputBoxStyles { readonly type?: string; readonly validationOptions?: IInputValidationOptions; readonly flexibleHeight?: boolean; + readonly flexibleWidth?: boolean; readonly flexibleMaxHeight?: number; readonly actions?: ReadonlyArray; } @@ -172,6 +174,13 @@ export class InputBox extends Widget { this.mirror.innerHTML = ' '; this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto }); + + if (this.options.flexibleWidth) { + this.input.setAttribute('wrap', 'off'); + this.mirror.style.whiteSpace = 'pre'; + this.mirror.style.wordWrap = 'initial'; + } + dom.append(container, this.scrollableElement.getDomNode()); this._register(this.scrollableElement); @@ -320,12 +329,36 @@ export class InputBox extends Widget { } public set width(width: number) { - this.input.style.width = width + 'px'; + if (this.options.flexibleHeight && this.options.flexibleWidth) { + // textarea with horizontal scrolling + let horizontalPadding = 0; + if (this.mirror) { + const paddingLeft = parseFloat(this.mirror.style.paddingLeft || '') || 0; + const paddingRight = parseFloat(this.mirror.style.paddingRight || '') || 0; + horizontalPadding = paddingLeft + paddingRight; + } + this.input.style.width = (width - horizontalPadding) + 'px'; + } else { + this.input.style.width = width + 'px'; + } + if (this.mirror) { this.mirror.style.width = width + 'px'; } } + public set paddingRight(paddingRight: number) { + if (this.options.flexibleHeight && this.options.flexibleWidth) { + this.input.style.width = `calc(100% - ${paddingRight}px)`; + } else { + this.input.style.paddingRight = paddingRight + 'px'; + } + + if (this.mirror) { + this.mirror.style.paddingRight = paddingRight + 'px'; + } + } + private updateScrollDimensions(): void { if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number') { return; @@ -438,7 +471,7 @@ export class InputBox extends Widget { div = dom.append(container, $('.monaco-inputbox-container')); layout(); - const renderOptions: RenderOptions = { + const renderOptions: MarkdownRenderOptions = { inline: true, className: 'monaco-inputbox-message' }; diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 4f5394f773d14..8ae8867bf645b 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -683,6 +683,22 @@ export class ListView implements ISpliceable, IDisposable { this.scrollableElement.setScrollPosition({ scrollTop }); } + getScrollLeft(): number { + const scrollPosition = this.scrollableElement.getScrollPosition(); + return scrollPosition.scrollLeft; + } + + setScrollLeftt(scrollLeft: number): void { + if (this.scrollableElementUpdateDisposable) { + this.scrollableElementUpdateDisposable.dispose(); + this.scrollableElementUpdateDisposable = null; + this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth }); + } + + this.scrollableElement.setScrollPosition({ scrollLeft }); + } + + get scrollTop(): number { return this.getScrollTop(); } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 6e82c6de867f1..15b21e3b9b0ff 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1307,6 +1307,14 @@ export class List implements ISpliceable, IDisposable { this.view.setScrollTop(scrollTop); } + get scrollLeft(): number { + return this.view.getScrollLeft(); + } + + set scrollLeft(scrollLeft: number) { + this.view.setScrollLeftt(scrollLeft); + } + get scrollHeight(): number { return this.view.scrollHeight; } diff --git a/src/vs/base/browser/ui/menu/check.svg b/src/vs/base/browser/ui/menu/check.svg index 865cc83c347af..cea818ef5968a 100644 --- a/src/vs/base/browser/ui/menu/check.svg +++ b/src/vs/base/browser/ui/menu/check.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts index 0b1154a98fdb2..75b3ba027dd28 100644 --- a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts +++ b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./octicons/octicons'; +import 'vs/css!./octicons/octicons2'; +import 'vs/css!./octicons/octicons-main'; import 'vs/css!./octicons/octicons-animations'; import { escape } from 'vs/base/common/strings'; @@ -30,4 +32,4 @@ export class OcticonLabel { set title(title: string) { this._container.title = title; } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons-main.css b/src/vs/base/browser/ui/octiconLabel/octicons/octicons-main.css new file mode 100644 index 0000000000000..841e5e575cde2 --- /dev/null +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons-main.css @@ -0,0 +1,21 @@ +body[data-octicons-update="enabled"] { + --version: octicons2; +} + +body { + --version: octicons; +} + +.octicon, .mega-octicon { + font-family: var(--version) !important; +} + +body[data-octicons-update="enabled"] .monaco-workbench .part.statusbar > .items-container > .statusbar-item span.octicon { + font-size: 16px; +} + +body[data-octicons-update="enabled"] .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css index d9cc1b7a4fa28..29ed93db1b12b 100644 --- a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css @@ -1,7 +1,7 @@ @font-face { font-family: "octicons"; - src: url("./octicons.ttf?1b0f2a9535896866c74dd24eedeb4374") format("truetype"), -url("./octicons.svg?1b0f2a9535896866c74dd24eedeb4374#octicons") format("svg"); + src: url("./octicons.ttf?dda6b6d46f87b1fa91a76fc0389eeb1d") format("truetype"), +url("./octicons.svg?dda6b6d46f87b1fa91a76fc0389eeb1d#octicons") format("svg"); } .octicon, .mega-octicon { @@ -169,7 +169,7 @@ url("./octicons.svg?1b0f2a9535896866c74dd24eedeb4374#octicons") format("svg"); .octicon-person-outline:before { content: "\f018" } .octicon-pin:before { content: "\f041" } .octicon-plug:before { content: "\f0d4" } -.octicon-plus-small:before { content: "\f28a" } +.octicon-plus-small:before { content: "\f05d" } .octicon-plus:before { content: "\f05d" } .octicon-primitive-dot:before { content: "\f052" } .octicon-primitive-square:before { content: "\f053" } @@ -233,16 +233,19 @@ url("./octicons.svg?1b0f2a9535896866c74dd24eedeb4374#octicons") format("svg"); .octicon-watch:before { content: "\f0e0" } .octicon-x:before { content: "\f081" } .octicon-zap:before { content: "\26a1" } +.octicon-error:before { content: "\26a2" } +.octicon-eye-closed:before { content: "\26a3" } +.octicon-fold-down:before { content: "\26a4" } +.octicon-fold-up:before { content: "\26a5" } +.octicon-github-action:before { content: "\26a6" } +.octicon-info-outline:before { content: "\26a7" } +.octicon-play:before { content: "\26a8" } +.octicon-remote:before { content: "\26a9" } +.octicon-request-changes:before { content: "\26aa" } +.octicon-smiley-outline:before { content: "\f27d" } +.octicon-warning:before { content: "\f02d" } +.octicon-controls:before { content: "\26ad" } +.octicon-event:before { content: "\26ae" } +.octicon-record-keys:before { content: "\26af" } .octicon-archive:before { content: "\f101" } .octicon-arrow-both:before { content: "\f102" } -.octicon-error:before { content: "\f103" } -.octicon-eye-closed:before { content: "\f104" } -.octicon-fold-down:before { content: "\f105" } -.octicon-fold-up:before { content: "\f106" } -.octicon-github-action:before { content: "\f107" } -.octicon-info-outline:before { content: "\f108" } -.octicon-play:before { content: "\f109" } -.octicon-remote:before { content: "\f10a" } -.octicon-request-changes:before { content: "\f10b" } -.octicon-smiley-outline:before { content: "\f10c" } -.octicon-warning:before { content: "\f10d" } diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg index 3f4ab4f180795..48f7d1b2220f3 100644 --- a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg @@ -167,10 +167,10 @@ unicode="" horiz-adv-x="750" d=" M687.5 507.5H62.5C28.125 507.5 0 479.375 0 445V195C0 160.625 28.125 132.5 62.5 132.5H687.5C721.875 132.5 750 160.625 750 195V445C750 479.375 721.875 507.5 687.5 507.5zM250 257.5H125V382.5H250V257.5zM437.5 257.5H312.5V382.5H437.5V257.5zM625 257.5H500V382.5H625V257.5z" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons2.ttf b/src/vs/base/browser/ui/octiconLabel/octicons/octicons2.ttf new file mode 100644 index 0000000000000..daa9a772ad2ea Binary files /dev/null and b/src/vs/base/browser/ui/octiconLabel/octicons/octicons2.ttf differ diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 134c64bbef43c..d144eb6305038 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -18,7 +18,7 @@ import { domEvent } from 'vs/base/browser/event'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ISelectBoxDelegate, ISelectOptionItem, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; import { isMacintosh } from 'vs/base/common/platform'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; const $ = dom.$; diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 02c583d1e069f..81090f39c2cce 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -24,12 +24,12 @@ const defaultStyles: ISplitViewStyles = { }; export interface ISplitViewOptions { - orientation?: Orientation; // default Orientation.VERTICAL - styles?: ISplitViewStyles; - orthogonalStartSash?: Sash; - orthogonalEndSash?: Sash; - inverseAltBehavior?: boolean; - proportionalLayout?: boolean; // default true + readonly orientation?: Orientation; // default Orientation.VERTICAL + readonly styles?: ISplitViewStyles; + readonly orthogonalStartSash?: Sash; + readonly orthogonalEndSash?: Sash; + readonly inverseAltBehavior?: boolean; + readonly proportionalLayout?: boolean; // default true } /** @@ -48,7 +48,7 @@ export interface IView { readonly onDidChange: Event; readonly priority?: LayoutPriority; readonly snap?: boolean; - layout(size: number, orientation: Orientation): void; + layout(size: number, orthogonalSize: number | undefined): void; setVisible?(visible: boolean): void; } @@ -117,21 +117,20 @@ abstract class ViewItem { if (typeof size === 'number') { this._size = size; this._cachedVisibleSize = undefined; + dom.addClass(container, 'visible'); } else { this._size = 0; this._cachedVisibleSize = size.cachedVisibleSize; } - - dom.addClass(container, 'visible'); } - layout(): void { + layout(_orthogonalSize: number | undefined): void { this.container.scrollTop = 0; this.container.scrollLeft = 0; } - layoutView(orientation: Orientation): void { - this.view.layout(this.size, orientation); + layoutView(orthogonalSize: number | undefined): void { + this.view.layout(this.size, orthogonalSize); } dispose(): IView { @@ -142,19 +141,19 @@ abstract class ViewItem { class VerticalViewItem extends ViewItem { - layout(): void { - super.layout(); + layout(orthogonalSize: number | undefined): void { + super.layout(orthogonalSize); this.container.style.height = `${this.size}px`; - this.layoutView(Orientation.VERTICAL); + this.layoutView(orthogonalSize); } } class HorizontalViewItem extends ViewItem { - layout(): void { - super.layout(); + layout(orthogonalSize: number | undefined): void { + super.layout(orthogonalSize); this.container.style.width = `${this.size}px`; - this.layoutView(Orientation.HORIZONTAL); + this.layoutView(orthogonalSize); } } @@ -205,6 +204,7 @@ export class SplitView extends Disposable { private sashContainer: HTMLElement; private viewContainer: HTMLElement; private size = 0; + private orthogonalSize: number | undefined; private contentSize = 0; private proportions: undefined | number[] = undefined; private viewItems: ViewItem[] = []; @@ -475,9 +475,10 @@ export class SplitView extends Disposable { return viewItem.cachedVisibleSize; } - layout(size: number): void { + layout(size: number, orthogonalSize?: number): void { const previousSize = Math.max(this.size, this.contentSize); this.size = size; + this.orthogonalSize = orthogonalSize; if (!this.proportions) { const indexes = range(this.viewItems.length); @@ -820,7 +821,7 @@ export class SplitView extends Disposable { this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); // Layout views - this.viewItems.forEach(item => item.layout()); + this.viewItems.forEach(item => item.layout(this.orthogonalSize)); // Layout sashes this.sashItems.forEach(item => item.sash.layout()); diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 7402d493a98c0..1aa9c5363e29f 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1319,6 +1319,14 @@ export abstract class AbstractTree implements IDisposable this.view.scrollTop = scrollTop; } + get scrollLeft(): number { + return this.view.scrollTop; + } + + set scrollLeft(scrollLeft: number) { + this.view.scrollLeft = scrollLeft; + } + get scrollHeight(): number { return this.view.scrollHeight; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index b26b80d2c4995..9c6bea61a4156 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -400,6 +400,14 @@ export class AsyncDataTree implements IDisposable this.tree.scrollTop = scrollTop; } + get scrollLeft(): number { + return this.tree.scrollLeft; + } + + set scrollLeft(scrollLeft: number) { + this.tree.scrollLeft = scrollLeft; + } + get scrollHeight(): number { return this.tree.scrollHeight; } diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index a568881e7c2ff..bdf19be09f5de 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -219,7 +219,7 @@ export class IndexTreeModel, TFilterData = voi const result = this._setListNodeCollapsed(node, listIndex, revealed, collapsed!, recursive || false); - if (this.autoExpandSingleChildren && !collapsed! && !recursive) { + if (node !== this.root && this.autoExpandSingleChildren && !collapsed! && !recursive) { let onlyVisibleChildIndex = -1; for (let i = 0; i < node.children.length; i++) { diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 7aa4259771428..cb3c94b3d1580 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -293,12 +293,9 @@ function topStep(array: ReadonlyArray, compare: (a: T, b: T) => number, re } /** - * @returns a new array with all falsy values removed. The original array IS NOT modified. + * @returns New array with all falsy values removed. The original array IS NOT modified. */ export function coalesce(array: ReadonlyArray): T[] { - if (!array) { - return array; - } return array.filter(e => !!e); } @@ -306,9 +303,6 @@ export function coalesce(array: ReadonlyArray): T[] { * Remove all falsey values from `array`. The original array IS modified. */ export function coalesceInPlace(array: Array): void { - if (!array) { - return; - } let to = 0; for (let i = 0; i < array.length; i++) { if (!!array[i]) { diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index da6f814332f62..b8077dd537647 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -194,3 +194,13 @@ export function getErrorMessage(err: any): string { return String(err); } + + +export class NotImplementedError extends Error { + constructor(message?: string) { + super('NotImplemented'); + if (message) { + this.message = message; + } + } +} diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index d01c32668caf6..fe233f874bf84 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -549,7 +549,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb const patternStartPos = patternPos; const wordStartPos = wordPos; - // There will be a mach, fill in tables + // There will be a match, fill in tables for (patternPos = patternStartPos + 1; patternPos <= patternLen; patternPos++) { for (wordPos = 1; wordPos <= wordLen; wordPos++) { @@ -573,6 +573,11 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb } else { score = 5; } + } else if (isSeparatorAtPos(wordLow, wordPos - 1) && (wordPos === 1 || !isSeparatorAtPos(wordLow, wordPos - 2))) { + // hitting a separator: `. <-> foo.bar` + // ^ + score = 5; + } else if (isSeparatorAtPos(wordLow, wordPos - 2) || isWhitespaceAtPos(wordLow, wordPos - 2)) { // post separator: `foo <-> bar_foo` // ^^^ diff --git a/src/vs/base/common/insane/cgmanifest.json b/src/vs/base/common/insane/cgmanifest.json new file mode 100644 index 0000000000000..bb94b81708873 --- /dev/null +++ b/src/vs/base/common/insane/cgmanifest.json @@ -0,0 +1,17 @@ +{ + "registrations": [ + { + "component": { + "type": "git", + "git": { + "name": "insane", + "repositoryUrl": "https://github.com/bevacqua/insane", + "commitHash": "7f5a809f44a37e7d11ee5343b2d8bca4be6ba4ae" + } + }, + "license": "MIT", + "version": "2.6.2" + } + ], + "version": 1 +} diff --git a/src/vs/base/common/insane/insane.d.ts b/src/vs/base/common/insane/insane.d.ts new file mode 100644 index 0000000000000..def347529e95f --- /dev/null +++ b/src/vs/base/common/insane/insane.d.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export as namespace insane; + +export = insane; + +declare function insane( + html: string, + options?: { + readonly allowedSchemes?: readonly string[], + readonly allowedTags?: readonly string[], + readonly allowedAttributes?: { readonly [key: string]: string[] }, + }, + strict?: boolean, +): string; + +declare namespace insane { } diff --git a/src/vs/base/common/insane/insane.js b/src/vs/base/common/insane/insane.js new file mode 100644 index 0000000000000..3cfdf5886bb01 --- /dev/null +++ b/src/vs/base/common/insane/insane.js @@ -0,0 +1,478 @@ +/* +The MIT License (MIT) + +Copyright © 2015 Nicolas Bevacqua + +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. +*/ + +// ESM-comment-begin +let __insane_exports; +// ESM-comment-end + + + +(function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o } return r })()({ + 1: [function (require, module, exports) { + 'use strict'; + + var toMap = require('./toMap'); + var uris = ['background', 'base', 'cite', 'href', 'longdesc', 'src', 'usemap']; + + module.exports = { + uris: toMap(uris) // attributes that have an href and hence need to be sanitized + }; + + }, { "./toMap": 10 }], 2: [function (require, module, exports) { + 'use strict'; + + var defaults = { + allowedAttributes: { + '*': ['title', 'accesskey'], + a: ['href', 'name', 'target', 'aria-label'], + iframe: ['allowfullscreen', 'frameborder', 'src'], + img: ['src', 'alt', 'title', 'aria-label'] + }, + allowedClasses: {}, + allowedSchemes: ['http', 'https', 'mailto'], + allowedTags: [ + 'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark', + 'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table', + 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul' + ], + filter: null + }; + + module.exports = defaults; + + }, {}], 3: [function (require, module, exports) { + 'use strict'; + + var toMap = require('./toMap'); + var voids = ['area', 'br', 'col', 'hr', 'img', 'wbr', 'input', 'base', 'basefont', 'link', 'meta']; + + module.exports = { + voids: toMap(voids) + }; + + }, { "./toMap": 10 }], 4: [function (require, module, exports) { + 'use strict'; + + var he = require('he'); + var assign = require('assignment'); + var parser = require('./parser'); + var sanitizer = require('./sanitizer'); + var defaults = require('./defaults'); + + function insane(html, options, strict) { + var buffer = []; + var configuration = strict === true ? options : assign({}, defaults, options); + var handler = sanitizer(buffer, configuration); + + parser(html, handler); + + return buffer.join(''); + } + + insane.defaults = defaults; + module.exports = insane; + __insane_exports = insane; + + }, { "./defaults": 2, "./parser": 7, "./sanitizer": 8, "assignment": 6, "he": 9 }], 5: [function (require, module, exports) { + 'use strict'; + + module.exports = function lowercase(string) { + return typeof string === 'string' ? string.toLowerCase() : string; + }; + + }, {}], 6: [function (require, module, exports) { + 'use strict'; + + function assignment(result) { + var stack = Array.prototype.slice.call(arguments, 1); + var item; + var key; + while (stack.length) { + item = stack.shift(); + for (key in item) { + if (item.hasOwnProperty(key)) { + if (Object.prototype.toString.call(result[key]) === '[object Object]') { + result[key] = assignment(result[key], item[key]); + } else { + result[key] = item[key]; + } + } + } + } + return result; + } + + module.exports = assignment; + + }, {}], 7: [function (require, module, exports) { + 'use strict'; + + var he = require('he'); + var lowercase = require('./lowercase'); + var attributes = require('./attributes'); + var elements = require('./elements'); + var rstart = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/; + var rend = /^<\s*\/\s*([\w:-]+)[^>]*>/; + var rattrs = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g; + var rtag = /^'); + if (index >= 0) { + if (handler.comment) { + handler.comment(html.substring(4, index)); + } + html = html.substring(index + 3); + chars = false; + } + } + + function parseTagDecode() { + if (!chars) { + return; + } + var text; + var index = html.indexOf('<'); + if (index >= 0) { + text = html.substring(0, index); + html = html.substring(index); + } else { + text = html; + html = ''; + } + if (handler.chars) { + handler.chars(text); + } + } + + function parseStartTag(tag, tagName, rest, unary) { + var attrs = {}; + var low = lowercase(tagName); + var u = elements.voids[low] || !!unary; + + rest.replace(rattrs, attrReplacer); + + if (!u) { + stack.push(low); + } + if (handler.start) { + handler.start(low, attrs, u); + } + + function attrReplacer(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + if (doubleQuotedValue === void 0 && singleQuotedValue === void 0 && unquotedValue === void 0) { + attrs[name] = void 0; // attribute is like + } else { + attrs[name] = he.decode(doubleQuotedValue || singleQuotedValue || unquotedValue || ''); + } + } + } + + function parseEndTag(tag, tagName) { + var i; + var pos = 0; + var low = lowercase(tagName); + if (low) { + for (pos = stack.length - 1; pos >= 0; pos--) { + if (stack[pos] === low) { + break; // find the closest opened tag of the same type + } + } + } + if (pos >= 0) { + for (i = stack.length - 1; i >= pos; i--) { + if (handler.end) { // close all the open elements, up the stack + handler.end(stack[i]); + } + } + stack.length = pos; + } + } + } + + module.exports = parser; + + }, { "./attributes": 1, "./elements": 3, "./lowercase": 5, "he": 9 }], 8: [function (require, module, exports) { + 'use strict'; + + var he = require('he'); + var lowercase = require('./lowercase'); + var attributes = require('./attributes'); + var elements = require('./elements'); + + function sanitizer(buffer, options) { + var last; + var context; + var o = options || {}; + + reset(); + + return { + start: start, + end: end, + chars: chars + }; + + function out(value) { + buffer.push(value); + } + + function start(tag, attrs, unary) { + var low = lowercase(tag); + + if (context.ignoring) { + ignore(low); return; + } + if ((o.allowedTags || []).indexOf(low) === -1) { + ignore(low); return; + } + if (o.filter && !o.filter({ tag: low, attrs: attrs })) { + ignore(low); return; + } + + out('<'); + out(low); + Object.keys(attrs).forEach(parse); + out(unary ? '/>' : '>'); + + function parse(key) { + var value = attrs[key]; + var classesOk = (o.allowedClasses || {})[low] || []; + var attrsOk = (o.allowedAttributes || {})[low] || []; + attrsOk = attrsOk.concat((o.allowedAttributes || {})['*'] || []); + var valid; + var lkey = lowercase(key); + if (lkey === 'class' && attrsOk.indexOf(lkey) === -1) { + value = value.split(' ').filter(isValidClass).join(' ').trim(); + valid = value.length; + } else { + valid = attrsOk.indexOf(lkey) !== -1 && (attributes.uris[lkey] !== true || testUrl(value)); + } + if (valid) { + out(' '); + out(key); + if (typeof value === 'string') { + out('="'); + out(he.encode(value)); + out('"'); + } + } + function isValidClass(className) { + return classesOk && classesOk.indexOf(className) !== -1; + } + } + } + + function end(tag) { + var low = lowercase(tag); + var allowed = (o.allowedTags || []).indexOf(low) !== -1; + if (allowed) { + if (context.ignoring === false) { + out(''); + } else { + unignore(low); + } + } else { + unignore(low); + } + } + + function testUrl(text) { + var start = text[0]; + if (start === '#' || start === '/') { + return true; + } + var colon = text.indexOf(':'); + if (colon === -1) { + return true; + } + var questionmark = text.indexOf('?'); + if (questionmark !== -1 && colon > questionmark) { + return true; + } + var hash = text.indexOf('#'); + if (hash !== -1 && colon > hash) { + return true; + } + return o.allowedSchemes.some(matches); + + function matches(scheme) { + return text.indexOf(scheme + ':') === 0; + } + } + + function chars(text) { + if (context.ignoring === false) { + out(o.transformText ? o.transformText(text) : text); + } + } + + function ignore(tag) { + if (elements.voids[tag]) { + return; + } + if (context.ignoring === false) { + context = { ignoring: tag, depth: 1 }; + } else if (context.ignoring === tag) { + context.depth++; + } + } + + function unignore(tag) { + if (context.ignoring === tag) { + if (--context.depth <= 0) { + reset(); + } + } + } + + function reset() { + context = { ignoring: false, depth: 0 }; + } + } + + module.exports = sanitizer; + + }, { "./attributes": 1, "./elements": 3, "./lowercase": 5, "he": 9 }], 9: [function (require, module, exports) { + 'use strict'; + + var escapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + var unescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'" + }; + var rescaped = /(&|<|>|"|')/g; + var runescaped = /[&<>"']/g; + + function escapeHtmlChar(match) { + return escapes[match]; + } + function unescapeHtmlChar(match) { + return unescapes[match]; + } + + function escapeHtml(text) { + return text == null ? '' : String(text).replace(runescaped, escapeHtmlChar); + } + + function unescapeHtml(html) { + return html == null ? '' : String(html).replace(rescaped, unescapeHtmlChar); + } + + escapeHtml.options = unescapeHtml.options = {}; + + module.exports = { + encode: escapeHtml, + escape: escapeHtml, + decode: unescapeHtml, + unescape: unescapeHtml, + version: '1.0.0-browser' + }; + + }, {}], 10: [function (require, module, exports) { + 'use strict'; + + function toMap(list) { + return list.reduce(asKey, {}); + } + + function asKey(accumulator, item) { + accumulator[item] = true; + return accumulator; + } + + module.exports = toMap; + + }, {}] +}, {}, [4]); + +// BEGIN MONACOCHANGE +// __marked_exports = marked; +// }).call(this); + +// ESM-comment-begin +define(function() { return __insane_exports; }); +// ESM-comment-end diff --git a/src/vs/base/common/insane/insane.license.txt b/src/vs/base/common/insane/insane.license.txt new file mode 100644 index 0000000000000..b980cef0bc7cd --- /dev/null +++ b/src/vs/base/common/insane/insane.license.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright © 2015 Nicolas Bevacqua + +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. diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 6be438cee416b..44cfdada8ab1b 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -127,8 +127,7 @@ export class DisposableStore implements IDisposable { markTracked(t); if (this._isDisposed) { - console.warn(new Error('Registering disposable on object that has already been disposed of').stack); - t.dispose(); + console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack); } else { this._toDispose.add(t); } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index f1cce9af4ceb1..277ab50bb88d9 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { CharCode } from 'vs/base/common/charCode'; import { Iterator, IteratorResult, FIN } from './iterator'; + export function values(set: Set): V[]; export function values(map: Map): V[]; export function values(forEachable: { forEach(callback: (value: V, ...more: any[]) => any): void }): V[] { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 46d2933a05e8c..4f93e06df0b0b 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; +import * as platform from 'vs/base/common/platform'; + export namespace Schemas { /** @@ -47,5 +50,48 @@ export namespace Schemas { export const vscodeRemote: string = 'vscode-remote'; + export const vscodeRemoteResource: string = 'vscode-remote-resource'; + export const userData: string = 'vscode-userdata'; } + +class RemoteAuthoritiesImpl { + private readonly _hosts: { [authority: string]: string; }; + private readonly _ports: { [authority: string]: number; }; + private readonly _connectionTokens: { [authority: string]: string; }; + private _preferredWebSchema: 'http' | 'https'; + + constructor() { + this._hosts = Object.create(null); + this._ports = Object.create(null); + this._connectionTokens = Object.create(null); + this._preferredWebSchema = 'http'; + } + + public setPreferredWebSchema(schema: 'http' | 'https') { + this._preferredWebSchema = schema; + } + + public set(authority: string, host: string, port: number): void { + this._hosts[authority] = host; + this._ports[authority] = port; + } + + public setConnectionToken(authority: string, connectionToken: string): void { + this._connectionTokens[authority] = connectionToken; + } + + public rewrite(authority: string, path: string): URI { + const host = this._hosts[authority]; + const port = this._ports[authority]; + const connectionToken = this._connectionTokens[authority]; + return URI.from({ + scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, + authority: `${host}:${port}`, + path: `/vscode-remote-resource`, + query: `path=${encodeURIComponent(path)}&tkn=${encodeURIComponent(connectionToken)}` + }); + } +} + +export const RemoteAuthorities = new RemoteAuthoritiesImpl(); diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index d7371552d30c9..07759dffe5979 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -93,14 +93,12 @@ export function PlatformToString(platform: Platform) { } let _platform: Platform = Platform.Web; -if (_isNative) { - if (_isMacintosh) { - _platform = Platform.Mac; - } else if (_isWindows) { - _platform = Platform.Windows; - } else if (_isLinux) { - _platform = Platform.Linux; - } +if (_isMacintosh) { + _platform = Platform.Mac; +} else if (_isWindows) { + _platform = Platform.Windows; +} else if (_isLinux) { + _platform = Platform.Linux; } export const isWindows = _isWindows; diff --git a/src/vs/base/common/process.ts b/src/vs/base/common/process.ts index a8447d58eea2c..7b9edd30963f5 100644 --- a/src/vs/base/common/process.ts +++ b/src/vs/base/common/process.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isWindows, isMacintosh, setImmediate } from 'vs/base/common/platform'; +import { isWindows, isMacintosh, setImmediate, IProcessEnvironment } from 'vs/base/common/platform'; interface IProcess { platform: string; - env: object; + env: IProcessEnvironment; cwd(): string; nextTick(callback: (...args: any[]) => void): number; diff --git a/src/vs/base/common/search.ts b/src/vs/base/common/search.ts new file mode 100644 index 0000000000000..743967964b65f --- /dev/null +++ b/src/vs/base/common/search.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as strings from './strings'; + +export function buildReplaceStringWithCasePreserved(matches: string[] | null, pattern: string): string { + if (matches && (matches[0] !== '')) { + if (matches[0].toUpperCase() === matches[0]) { + return pattern.toUpperCase(); + } else if (matches[0].toLowerCase() === matches[0]) { + return pattern.toLowerCase(); + } else if (strings.containsUppercaseCharacter(matches[0][0])) { + if (validateSpecificSpecialCharacter(matches, pattern, '-')) { + return buildReplaceStringForSpecificSpecialCharacter(matches, pattern, '-'); + } else { + return pattern[0].toUpperCase() + pattern.substr(1); + } + } else { + // we don't understand its pattern yet. + return pattern; + } + } else { + return pattern; + } +} + +function validateSpecificSpecialCharacter(matches: string[], pattern: string, specialCharacter: string): boolean { + const doesConatinSpecialCharacter = matches[0].indexOf(specialCharacter) !== -1 && pattern.indexOf(specialCharacter) !== -1; + return doesConatinSpecialCharacter && matches[0].split(specialCharacter).length === pattern.split(specialCharacter).length; +} + +function buildReplaceStringForSpecificSpecialCharacter(matches: string[], pattern: string, specialCharacter: string): string { + const splitPatternAtSpecialCharacter = pattern.split(specialCharacter); + const splitMatchAtSpecialCharacter = matches[0].split(specialCharacter); + let replaceString: string = ''; + splitPatternAtSpecialCharacter.forEach((splitValue, index) => { + replaceString += buildReplaceStringWithCasePreserved([splitMatchAtSpecialCharacter[index]], splitValue) + specialCharacter; + }); + + return replaceString.slice(0, -1); +} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index d4397d3279de5..71185d0704613 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -729,3 +729,18 @@ export function getNLines(str: string, n = 1): string { str.substr(0, idx) : str; } + +/** + * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. + */ +export function singleLetterHash(n: number): string { + const LETTERS_CNT = (CharCode.Z - CharCode.A + 1); + + n = n % (2 * LETTERS_CNT); + + if (n < LETTERS_CNT) { + return String.fromCharCode(CharCode.a + n); + } + + return String.fromCharCode(CharCode.A + n - LETTERS_CNT); +} diff --git a/src/vs/base/common/worker/simpleWorker.ts b/src/vs/base/common/worker/simpleWorker.ts index 2cc1b188cac88..6c9815d3aaa67 100644 --- a/src/vs/base/common/worker/simpleWorker.ts +++ b/src/vs/base/common/worker/simpleWorker.ts @@ -16,7 +16,7 @@ export interface IWorker extends IDisposable { } export interface IWorkerCallback { - (message: string): void; + (message: any): void; } export interface IWorkerFactory { diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 459a5b94bd8dd..1e2fdbccec466 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -8,8 +8,7 @@ import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import * as platform from 'vs/base/common/platform'; - -declare var process: any; +import * as process from 'vs/base/common/process'; export interface ISocket extends IDisposable { onData(listener: (e: VSBuffer) => void): IDisposable; @@ -406,46 +405,53 @@ export class Client extends IPCClient { /** * Will ensure no messages are lost if there are no event listeners. */ -export function createBufferedEvent(source: Event): Event { - let emitter: Emitter; - let hasListeners = false; - let isDeliveringMessages = false; - let bufferedMessages: T[] = []; - - const deliverMessages = () => { - if (isDeliveringMessages) { +export class BufferedEmitter { + private _emitter: Emitter; + public readonly event: Event; + + private _hasListeners = false; + private _isDeliveringMessages = false; + private _bufferedMessages: T[] = []; + + constructor() { + this._emitter = new Emitter({ + onFirstListenerAdd: () => { + this._hasListeners = true; + // it is important to deliver these messages after this call, but before + // other messages have a chance to be received (to guarantee in order delivery) + // that's why we're using here nextTick and not other types of timeouts + process.nextTick(() => this._deliverMessages); + }, + onLastListenerRemove: () => { + this._hasListeners = false; + } + }); + + this.event = this._emitter.event; + } + + private _deliverMessages(): void { + if (this._isDeliveringMessages) { return; } - isDeliveringMessages = true; - while (hasListeners && bufferedMessages.length > 0) { - emitter.fire(bufferedMessages.shift()!); + this._isDeliveringMessages = true; + while (this._hasListeners && this._bufferedMessages.length > 0) { + this._emitter.fire(this._bufferedMessages.shift()!); } - isDeliveringMessages = false; - }; + this._isDeliveringMessages = false; + } - source((e: T) => { - bufferedMessages.push(e); - deliverMessages(); - }); - - emitter = new Emitter({ - onFirstListenerAdd: () => { - hasListeners = true; - // it is important to deliver these messages after this call, but before - // other messages have a chance to be received (to guarantee in order delivery) - // that's why we're using here nextTick and not other types of timeouts - if (typeof process !== 'undefined') { - process.nextTick(deliverMessages); - } else { - platform.setImmediate(deliverMessages); - } - }, - onLastListenerRemove: () => { - hasListeners = false; + public fire(event: T): void { + if (this._hasListeners) { + this._emitter.fire(event); + } else { + this._bufferedMessages.push(event); } - }); + } - return emitter.event; + public flushBuffer(): void { + this._bufferedMessages = []; + } } class QueueElement { @@ -535,20 +541,20 @@ export class PersistentProtocol implements IMessagePassingProtocol { private _socketReader: ProtocolReader; private _socketDisposables: IDisposable[]; - private _onControlMessage = new Emitter(); - readonly onControlMessage: Event = createBufferedEvent(this._onControlMessage.event); + private readonly _onControlMessage = new BufferedEmitter(); + readonly onControlMessage: Event = this._onControlMessage.event; - private _onMessage = new Emitter(); - readonly onMessage: Event = createBufferedEvent(this._onMessage.event); + private readonly _onMessage = new BufferedEmitter(); + readonly onMessage: Event = this._onMessage.event; - private _onClose = new Emitter(); - readonly onClose: Event = createBufferedEvent(this._onClose.event); + private readonly _onClose = new BufferedEmitter(); + readonly onClose: Event = this._onClose.event; - private _onSocketClose = new Emitter(); - readonly onSocketClose: Event = createBufferedEvent(this._onSocketClose.event); + private readonly _onSocketClose = new BufferedEmitter(); + readonly onSocketClose: Event = this._onSocketClose.event; - private _onSocketTimeout = new Emitter(); - readonly onSocketTimeout: Event = createBufferedEvent(this._onSocketTimeout.event); + private readonly _onSocketTimeout = new BufferedEmitter(); + readonly onSocketTimeout: Event = this._onSocketTimeout.event; public get unacknowledgedCount(): number { return this._outgoingMsgId - this._outgoingAckId; @@ -661,6 +667,10 @@ export class PersistentProtocol implements IMessagePassingProtocol { this._isReconnecting = true; this._socketDisposables = dispose(this._socketDisposables); + this._onControlMessage.flushBuffer(); + this._onSocketClose.flushBuffer(); + this._onSocketTimeout.flushBuffer(); + this._socket.dispose(); this._socket = socket; this._socketWriter = new ProtocolWriter(this._socket); diff --git a/src/vs/base/parts/ipc/node/ipc.cp.ts b/src/vs/base/parts/ipc/node/ipc.cp.ts index 9104deb503c02..42671a2e1a6b3 100644 --- a/src/vs/base/parts/ipc/node/ipc.cp.ts +++ b/src/vs/base/parts/ipc/node/ipc.cp.ts @@ -86,7 +86,7 @@ export interface IIPCOptions { export class Client implements IChannelClient, IDisposable { - private disposeDelayer: Delayer; + private disposeDelayer: Delayer | undefined; private activeRequests = new Set(); private child: ChildProcess | null; private _client: IPCClient | null; @@ -137,7 +137,7 @@ export class Client implements IChannelClient, IDisposable { cancellationTokenListener.dispose(); this.activeRequests.delete(disposable); - if (this.activeRequests.size === 0) { + if (this.activeRequests.size === 0 && this.disposeDelayer) { this.disposeDelayer.trigger(() => this.disposeClient()); } }); @@ -271,8 +271,10 @@ export class Client implements IChannelClient, IDisposable { dispose() { this._onDidProcessExit.dispose(); - this.disposeDelayer.cancel(); - this.disposeDelayer = null!; // StrictNullOverride: nulling out ok in dispose + if (this.disposeDelayer) { + this.disposeDelayer.cancel(); + this.disposeDelayer = undefined; + } this.disposeClient(); this.activeRequests.clear(); } diff --git a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts index bb9dec7560598..fdd8bcbf06b5b 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts @@ -10,7 +10,6 @@ import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; import { IAction, IActionRunner } from 'vs/base/common/actions'; -import { compareAnything } from 'vs/base/common/comparers'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import * as DOM from 'vs/base/browser/dom'; @@ -576,37 +575,3 @@ export class QuickOpenModel implements return entry.run(mode, context); } } - -/** - * A good default sort implementation for quick open entries respecting highlight information - * as well as associated resources. - */ -export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number { - - // Give matches with label highlights higher priority over - // those with only description highlights - const labelHighlightsA = elementA.getHighlights()[0] || []; - const labelHighlightsB = elementB.getHighlights()[0] || []; - if (labelHighlightsA.length && !labelHighlightsB.length) { - return -1; - } - - if (!labelHighlightsA.length && labelHighlightsB.length) { - return 1; - } - - // Fallback to the full path if labels are identical and we have associated resources - let nameA = elementA.getLabel()!; - let nameB = elementB.getLabel()!; - if (nameA === nameB) { - const resourceA = elementA.getResource(); - const resourceB = elementB.getResource(); - - if (resourceA && resourceB) { - nameA = resourceA.fsPath; - nameB = resourceB.fsPath; - } - } - - return compareAnything(nameA, nameB, lookFor); -} diff --git a/src/vs/base/parts/quickopen/common/quickOpen.ts b/src/vs/base/parts/quickopen/common/quickOpen.ts index dd39a215ef17b..d6039c688028b 100644 --- a/src/vs/base/parts/quickopen/common/quickOpen.ts +++ b/src/vs/base/parts/quickopen/common/quickOpen.ts @@ -68,6 +68,8 @@ export interface IDataSource { export interface IRenderer { getHeight(entry: T): number; getTemplateId(entry: T): string; + // rationale: will be replaced by quickinput later + // tslint:disable-next-line: no-dom-globals renderTemplate(templateId: string, container: HTMLElement, styles: any): any; renderElement(entry: T, templateId: string, templateData: any, styles: any): void; disposeTemplate(templateId: string, templateData: any): void; @@ -92,4 +94,4 @@ export interface IModel { runner: IRunner; filter?: IFilter; accessibilityProvider?: IAccessiblityProvider; -} \ No newline at end of file +} diff --git a/src/vs/base/parts/request/browser/request.ts b/src/vs/base/parts/request/browser/request.ts new file mode 100644 index 0000000000000..f8532bbe6085e --- /dev/null +++ b/src/vs/base/parts/request/browser/request.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; +import { assign } from 'vs/base/common/objects'; +import { VSBuffer, bufferToStream } from 'vs/base/common/buffer'; +import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; + +export function request(options: IRequestOptions, token: CancellationToken): Promise { + if (options.proxyAuthorization) { + options.headers = assign(options.headers || {}, { 'Proxy-Authorization': options.proxyAuthorization }); + } + + const xhr = new XMLHttpRequest(); + return new Promise((resolve, reject) => { + + xhr.open(options.type || 'GET', options.url || '', true, options.user, options.password); + setRequestHeaders(xhr, options); + + xhr.responseType = 'arraybuffer'; + xhr.onerror = e => reject(new Error(xhr.statusText && ('XHR failed: ' + xhr.statusText))); + xhr.onload = (e) => { + resolve({ + res: { + statusCode: xhr.status, + headers: getResponseHeaders(xhr) + }, + stream: bufferToStream(VSBuffer.wrap(new Uint8Array(xhr.response))) + }); + }; + xhr.ontimeout = e => reject(new Error(`XHR timeout: ${options.timeout}ms`)); + + if (options.timeout) { + xhr.timeout = options.timeout; + } + + xhr.send(options.data); + + // cancel + token.onCancellationRequested(() => { + xhr.abort(); + reject(canceled()); + }); + }); +} + +function setRequestHeaders(xhr: XMLHttpRequest, options: IRequestOptions): void { + if (options.headers) { + outer: for (let k in options.headers) { + switch (k) { + case 'User-Agent': + case 'Accept-Encoding': + case 'Content-Length': + // unsafe headers + continue outer; + } + xhr.setRequestHeader(k, options.headers[k]); + } + } +} + +function getResponseHeaders(xhr: XMLHttpRequest): { [name: string]: string } { + const headers: { [name: string]: string } = Object.create(null); + for (const line of xhr.getAllResponseHeaders().split(/\r\n|\n|\r/g)) { + if (line) { + const idx = line.indexOf(':'); + headers[line.substr(0, idx).trim().toLowerCase()] = line.substr(idx + 1).trim(); + } + } + return headers; +} diff --git a/src/vs/base/parts/request/common/request.ts b/src/vs/base/parts/request/common/request.ts new file mode 100644 index 0000000000000..b8f75d72cf6c3 --- /dev/null +++ b/src/vs/base/parts/request/common/request.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBufferReadableStream } from 'vs/base/common/buffer'; + +export interface IHeaders { + [header: string]: string; +} + +export interface IRequestOptions { + type?: string; + url?: string; + user?: string; + password?: string; + headers?: IHeaders; + timeout?: number; + data?: string; + followRedirects?: number; + proxyAuthorization?: string; +} + +export interface IRequestContext { + res: { + headers: IHeaders; + statusCode?: number; + }; + stream: VSBufferReadableStream; +} diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 6cebac12c9feb..edf313654aa0c 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -206,7 +206,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { return this.doUpdateItems(recoveryConnection, { insert: recovery() }).then(() => closeRecoveryConnection(), error => { // In case of an error updating items, still ensure to close the connection - // to prevent SQLITE_BUSY errors when the connection is restablished + // to prevent SQLITE_BUSY errors when the connection is reestablished closeRecoveryConnection(); return Promise.reject(error); diff --git a/src/vs/base/test/browser/htmlContent.test.ts b/src/vs/base/test/browser/formattedTextRenderer.test.ts similarity index 65% rename from src/vs/base/test/browser/htmlContent.test.ts rename to src/vs/base/test/browser/formattedTextRenderer.test.ts index d5dec0729d9f1..0be9747852205 100644 --- a/src/vs/base/test/browser/htmlContent.test.ts +++ b/src/vs/base/test/browser/formattedTextRenderer.test.ts @@ -2,12 +2,12 @@ * 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 * as marked from 'vs/base/common/marked/marked'; -import { renderMarkdown, renderText, renderFormattedText } from 'vs/base/browser/htmlContentRenderer'; +import { renderText, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { DisposableStore } from 'vs/base/common/lifecycle'; -suite('HtmlContent', () => { +suite('FormattedTextRenderer', () => { const store = new DisposableStore(); setup(() => { @@ -101,36 +101,4 @@ suite('HtmlContent', () => { assert.strictEqual(result.children.length, 0); assert.strictEqual(result.innerHTML, '**bold**'); }); - test('image rendering conforms to default', () => { - const markdown = { value: `![image](someimageurl 'caption')` }; - const result: HTMLElement = renderMarkdown(markdown); - const renderer = new marked.Renderer(); - const imageFromMarked = marked(markdown.value, { - sanitize: true, - renderer - }).trim(); - assert.strictEqual(result.innerHTML, imageFromMarked); - }); - test('image rendering conforms to default without title', () => { - const markdown = { value: `![image](someimageurl)` }; - const result: HTMLElement = renderMarkdown(markdown); - const renderer = new marked.Renderer(); - const imageFromMarked = marked(markdown.value, { - sanitize: true, - renderer - }).trim(); - assert.strictEqual(result.innerHTML, imageFromMarked); - }); - test('image width from title params', () => { - let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` }); - assert.strictEqual(result.innerHTML, `

image

`); - }); - test('image height from title params', () => { - let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` }); - assert.strictEqual(result.innerHTML, `

image

`); - }); - test('image width and height from title params', () => { - let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` }); - assert.strictEqual(result.innerHTML, `

image

`); - }); }); diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts new file mode 100644 index 0000000000000..525eb86e741b8 --- /dev/null +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as marked from 'vs/base/common/marked/marked'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; + +suite('MarkdownRenderer', () => { + test('image rendering conforms to default', () => { + const markdown = { value: `![image](someimageurl 'caption')` }; + const result: HTMLElement = renderMarkdown(markdown); + const renderer = new marked.Renderer(); + const imageFromMarked = marked(markdown.value, { + sanitize: true, + renderer + }).trim(); + assert.strictEqual(result.innerHTML, imageFromMarked); + }); + + test('image rendering conforms to default without title', () => { + const markdown = { value: `![image](someimageurl)` }; + const result: HTMLElement = renderMarkdown(markdown); + const renderer = new marked.Renderer(); + const imageFromMarked = marked(markdown.value, { + sanitize: true, + renderer + }).trim(); + assert.strictEqual(result.innerHTML, imageFromMarked); + }); + + test('image width from title params', () => { + let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` }); + assert.strictEqual(result.innerHTML, `

image

`); + }); + + test('image height from title params', () => { + let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` }); + assert.strictEqual(result.innerHTML, `

image

`); + }); + + test('image width and height from title params', () => { + let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` }); + assert.strictEqual(result.innerHTML, `

image

`); + }); +}); diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index 25468a06bb9ae..d8dbad25721bd 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -9,6 +9,7 @@ import { TestView, nodesToArrays } from './util'; import { deepClone } from 'vs/base/common/objects'; // Simple example: +// // +-----+---------------+ // | 4 | 2 | // +-----+---------+-----+ @@ -16,6 +17,16 @@ import { deepClone } from 'vs/base/common/objects'; // +---------------+ 3 | // | 5 | | // +---------------+-----+ +// +// V +// +-H +// | +-4 +// | +-2 +// +-H +// | +-V +// | +-1 +// | +-5 +// +-3 suite('Grid', function () { let container: HTMLElement; @@ -511,7 +522,8 @@ suite('SerializableGrid', function () { container.appendChild(grid.element); grid.layout(800, 600); - assert.deepEqual(grid.serialize(), { + const actual = grid.serialize(); + assert.deepEqual(actual, { orientation: 0, width: 800, height: 600, diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 504e665f21a8c..c5e9cfce763d4 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; -import { SplitView, IView, Orientation, Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; +import { SplitView, IView, Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; import { Sash, SashState } from 'vs/base/browser/ui/sash/sash'; class TestView implements IView { @@ -27,7 +27,9 @@ class TestView implements IView { private _size = 0; get size(): number { return this._size; } - private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>(); + private _orthogonalSize: number | undefined = 0; + get orthogonalSize(): number | undefined { return this._orthogonalSize; } + private _onDidLayout = new Emitter<{ size: number; orthogonalSize: number | undefined }>(); readonly onDidLayout = this._onDidLayout.event; private _onDidFocus = new Emitter(); @@ -41,9 +43,10 @@ class TestView implements IView { assert(_minimumSize <= _maximumSize, 'splitview view minimum size must be <= maximum size'); } - layout(size: number, orientation: Orientation): void { + layout(size: number, orthogonalSize: number | undefined): void { this._size = size; - this._onDidLayout.fire({ size, orientation }); + this._orthogonalSize = orthogonalSize; + this._onDidLayout.fire({ size, orthogonalSize }); } focus(): void { @@ -523,4 +526,24 @@ suite('Splitview', () => { view2.dispose(); view1.dispose(); }); -}); \ No newline at end of file + + test('orthogonal size propagates to views', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY, LayoutPriority.Low); + const splitview = new SplitView(container, { proportionalLayout: false }); + splitview.layout(200); + + splitview.addView(view1, Sizing.Distribute); + splitview.addView(view2, Sizing.Distribute); + splitview.addView(view3, Sizing.Distribute); + + splitview.layout(200, 100); + assert.deepEqual([view1.orthogonalSize, view2.orthogonalSize, view3.orthogonalSize], [100, 100, 100]); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); +}); diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 25cc50d578043..42b741436610b 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -366,6 +366,10 @@ suite('Filters', () => { assertMatches('f', ':foo', ':^foo', fuzzyScore); }); + test('Separator only match should not be weak #79558', function () { + assertMatches('.', 'foo.bar', 'foo^.bar', fuzzyScore); + }); + test('Cannot set property \'1\' of undefined, #26511', function () { let word = new Array(123).join('a'); let pattern = new Array(120).join('a'); diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 46d8a254b5b54..4d15ad2046c4c 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IDisposable, dispose, ReferenceCollection, Disposable as DisposableBase, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, ReferenceCollection } from 'vs/base/common/lifecycle'; class Disposable implements IDisposable { isDisposed = false; @@ -50,38 +50,6 @@ suite('Lifecycle', () => { }); }); -suite('DisposableBase', () => { - test('register should not leak if object has already been disposed', () => { - let aCount = 0; - let bCount = 0; - - const disposable = new class extends DisposableBase { - register(other: IDisposable) { - this._register(other); - } - }; - - disposable.register(toDisposable(() => ++aCount)); - - assert.strictEqual(aCount, 0); - assert.strictEqual(bCount, 0); - - disposable.dispose(); - assert.strictEqual(aCount, 1); - assert.strictEqual(bCount, 0); - - // Any newly added disposables should be disposed of immediately - disposable.register(toDisposable(() => ++bCount)); - assert.strictEqual(aCount, 1); - assert.strictEqual(bCount, 1); - - // Further dispose calls should have no effect - disposable.dispose(); - assert.strictEqual(aCount, 1); - assert.strictEqual(bCount, 1); - }); -}); - suite('Reference Collection', () => { class Collection extends ReferenceCollection { private _count = 0; @@ -118,4 +86,4 @@ suite('Reference Collection', () => { ref4.dispose(); assert.equal(collection.count, 0); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/path.test.ts b/src/vs/base/test/common/path.test.ts index cfceaedc9e9cb..f5230c4a35c73 100644 --- a/src/vs/base/test/common/path.test.ts +++ b/src/vs/base/test/common/path.test.ts @@ -30,6 +30,7 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; +import * as process from 'vs/base/common/process'; suite('Paths (Node Implementation)', () => { test('join', () => { diff --git a/src/vs/base/test/node/keytar.test.ts b/src/vs/base/test/node/keytar.test.ts index 141e6ab904fa7..96ca335e940c9 100644 --- a/src/vs/base/test/node/keytar.test.ts +++ b/src/vs/base/test/node/keytar.test.ts @@ -32,4 +32,4 @@ suite('Keytar', () => { } })().then(done, done); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index c2a7ddc54e6a2..bce60b6b85564 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -19,26 +19,31 @@ function getWorker(workerId: string, label: string): Worker | Promise { // ESM-comment-begin if (typeof require === 'function') { // check if the JS lives on a different origin - const workerMain = require.toUrl('./' + workerId); - if (/^(http:)|(https:)|(file:)/.test(workerMain)) { - const currentUrl = String(window.location); - const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); - if (workerMain.substring(0, currentOrigin.length) !== currentOrigin) { - // this is the cross-origin case - // i.e. the webpage is running at a different origin than where the scripts are loaded from - const workerBaseUrl = workerMain.substr(0, workerMain.length - 'vs/base/worker/workerMain.js'.length); - const js = `/*${label}*/self.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};importScripts('${workerMain}');/*${label}*/`; - const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`; - return new Worker(url); - } - } - return new Worker(workerMain + '#' + label); + const workerUrl = getWorkerBootstrapUrl(workerMain, label); + return new Worker(workerUrl, { name: label }); } // ESM-comment-end throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`); } +export function getWorkerBootstrapUrl(scriptPath: string, label: string): string { + if (/^(http:)|(https:)|(file:)/.test(scriptPath)) { + const currentUrl = String(window.location); + const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); + if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) { + // this is the cross-origin case + // i.e. the webpage is running at a different origin than where the scripts are loaded from + const myPath = 'vs/base/worker/defaultWorkerFactory.js'; + const workerBaseUrl = require.toUrl(myPath).slice(0, -myPath.length); + const js = `/*${label}*/self.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};importScripts('${scriptPath}');/*${label}*/`; + const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`; + return url; + } + } + return scriptPath + '#' + label; +} + function isPromiseLike(obj: any): obj is PromiseLike { if (typeof obj.then === 'function') { return true; diff --git a/src/vs/base/worker/workerMain.ts b/src/vs/base/worker/workerMain.ts index 0ec8b7834dc9d..8d51252951a73 100644 --- a/src/vs/base/worker/workerMain.ts +++ b/src/vs/base/worker/workerMain.ts @@ -20,8 +20,8 @@ let loadCode = function (moduleId: string) { require([moduleId], function (ws) { setTimeout(function () { - let messageHandler = ws.create((msg: any) => { - (self).postMessage(msg); + let messageHandler = ws.create((msg: any, transfer?: Transferable[]) => { + (self).postMessage(msg, transfer); }, null); self.onmessage = (e) => messageHandler.onmessage(e.data); diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 5b06636edbbc6..663cc643f97fd 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -4,32 +4,31 @@ - - - + + + - + - - + + + - - - - - - - - + + + + diff --git a/src/vs/code/browser/workbench/workbench.js b/src/vs/code/browser/workbench/workbench.js index 65fae7c82df83..5687627518e66 100644 --- a/src/vs/code/browser/workbench/workbench.js +++ b/src/vs/code/browser/workbench/workbench.js @@ -3,25 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check 'use strict'; (function () { - require.config({ - baseUrl: `${window.location.origin}/out`, + /** @type any */ + const amdLoader = require; + + amdLoader.config({ + baseUrl: `${window.location.origin}/static/out`, paths: { - 'vscode-textmate': `${window.location.origin}/node_modules/vscode-textmate/release/main`, - 'onigasm-umd': `${window.location.origin}/node_modules/onigasm-umd/release/main`, - 'xterm': `${window.location.origin}/node_modules/xterm/lib/xterm.js`, - 'xterm-addon-search': `${window.location.origin}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, - 'xterm-addon-web-links': `${window.location.origin}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, - 'semver-umd': `${window.location.origin}/node_modules/semver-umd/lib/semver-umd.js`, + 'vscode-textmate': `${window.location.origin}/static/node_modules/vscode-textmate/release/main`, + 'onigasm-umd': `${window.location.origin}/static/node_modules/onigasm-umd/release/main`, + 'xterm': `${window.location.origin}/static/node_modules/xterm/lib/xterm.js`, + 'xterm-addon-search': `${window.location.origin}/static/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, + 'xterm-addon-web-links': `${window.location.origin}/static/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, + 'semver-umd': `${window.location.origin}/static/node_modules/semver-umd/lib/semver-umd.js`, + '@microsoft/applicationinsights-web': `${window.location.origin}/static/node_modules/@microsoft/applicationinsights-web/dist/applicationinsights-web.js`, } }); - require(['vs/workbench/workbench.web.api'], function (api) { + amdLoader(['vs/workbench/workbench.web.api'], function (api) { const options = JSON.parse(document.getElementById('vscode-workbench-web-configuration').getAttribute('data-settings')); api.create(document.body, options); }); -})(); \ No newline at end of file +})(); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index e6ee71c50f002..591cbed96f333 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -31,7 +31,7 @@ import { WindowsService } from 'vs/platform/windows/electron-browser/windowsServ import { MainProcessService, IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/electron-browser/issue/issueReporterModel'; -import { IssueReporterData, IssueReporterStyles, IssueType, ISettingsSearchIssueReporterData, IssueReporterFeatures, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; +import { IssueReporterData, IssueReporterStyles, IssueType, ISettingsSearchIssueReporterData, IssueReporterFeatures, IssueReporterExtensionData } from 'vs/platform/issue/node/issue'; import BaseHtml from 'vs/code/electron-browser/issue/issueReporterPage'; import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { ILogService, getLogLevel } from 'vs/platform/log/common/log'; @@ -39,7 +39,7 @@ import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { normalizeGitHubUrl } from 'vs/code/electron-browser/issue/issueReporterUtil'; import { Button } from 'vs/base/browser/ui/button/button'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; const MAX_URL_LENGTH = 2045; diff --git a/src/vs/code/electron-browser/issue/issueReporterModel.ts b/src/vs/code/electron-browser/issue/issueReporterModel.ts index c1f78969153e6..6c7fa528e3770 100644 --- a/src/vs/code/electron-browser/issue/issueReporterModel.ts +++ b/src/vs/code/electron-browser/issue/issueReporterModel.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { assign } from 'vs/base/common/objects'; -import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; -import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/node/issue'; +import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; export interface IssueReporterData { issueType: IssueType; diff --git a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts b/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts index 95857638e41a4..c69454639917f 100644 --- a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts +++ b/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IssueReporterModel } from 'vs/code/electron-browser/issue/issueReporterModel'; import { normalizeGitHubUrl } from 'vs/code/electron-browser/issue/issueReporterUtil'; -import { IssueType } from 'vs/platform/issue/common/issue'; +import { IssueType } from 'vs/platform/issue/node/issue'; suite('IssueReporter', () => { diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 321a5611e746b..557957dfd0b26 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -9,7 +9,7 @@ import { repeat } from 'vs/base/common/strings'; import { totalmem } from 'os'; import product from 'vs/platform/product/node/product'; import { localize } from 'vs/nls'; -import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/node/issue'; import * as browser from 'vs/base/browser/browser'; import * as platform from 'vs/base/common/platform'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; @@ -17,7 +17,7 @@ import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; import { addDisposableListener } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; let mapPidToWindowTitle = new Map(); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 905e9fd88dbc8..c5514f4eff7ca 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -48,15 +48,13 @@ import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/ import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; -import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; -import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { DiagnosticsChannel } from 'vs/platform/diagnostics/node/diagnosticsIpc'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/product'; -import { ProductService } from 'vs/platform/product/node/productService'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -112,10 +110,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat await configurationService.initialize(); services.set(IEnvironmentService, environmentService); + services.set(IProductService, { _serviceBrand: undefined, ...product }); services.set(ILogService, logService); services.set(IConfigurationService, configurationService); services.set(IRequestService, new SyncDescriptor(RequestService)); - services.set(IProductService, new SyncDescriptor(ProductService)); const mainProcessService = new MainProcessService(server, mainRouter); services.set(IMainProcessService, mainProcessService); diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index dfac360344acc..693082bb9edaa 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -3,11 +3,11 @@ - + - \ No newline at end of file + diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index cdbe49c1b3a3d..22d5dbe329c81 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -14,7 +14,10 @@ const bootstrapWindow = require('../../../../bootstrap-window'); // Setup shell environment process['lazyEnv'] = getLazyEnv(); -// Load workbench main +// Load workbench main JS, CSS and NLS all in parallel. This is an +// optimization to prevent a waterfall of loading to happen, because +// we know for a fact that workbench.desktop.main will depend on +// the related CSS and NLS counterparts. bootstrapWindow.load([ 'vs/workbench/workbench.desktop.main', 'vs/nls!vs/workbench/workbench.desktop.main', @@ -27,7 +30,7 @@ bootstrapWindow.load([ perf.mark('main/startup'); // @ts-ignore - return require('vs/workbench/electron-browser/main').main(configuration); + return require('vs/workbench/electron-browser/desktop.main').main(configuration); }); }, { removeDeveloperKeybindingsAfterLoad: true, diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d71ddf83ea18a..66c0baf040184 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -7,7 +7,7 @@ import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, p import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/platform'; import { WindowsManager } from 'vs/code/electron-main/windows'; import { IWindowsService, OpenContext, ActiveWindowManager, IURIToOpen } from 'vs/platform/windows/common/windows'; -import { WindowsChannel } from 'vs/platform/windows/node/windowsIpc'; +import { WindowsChannel } from 'vs/platform/windows/common/windowsIpc'; import { WindowsService } from 'vs/platform/windows/electron-main/windowsService'; import { ILifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; @@ -39,7 +39,6 @@ import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; -import { withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { WorkspacesChannel } from 'vs/platform/workspaces/node/workspacesIpc'; import { IWorkspacesMainService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; @@ -47,15 +46,14 @@ import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; -import { IIssueService } from 'vs/platform/issue/common/issue'; +import { IIssueService } from 'vs/platform/issue/node/issue'; import { IssueChannel } from 'vs/platform/issue/node/issueIpc'; import { IssueService } from 'vs/platform/issue/electron-main/issueService'; import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc'; import { setUnexpectedErrorHandler, onUnexpectedError } from 'vs/base/common/errors'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; -import { connectRemoteAgentManagement, ManagementPersistentConnection, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { IMenubarService } from 'vs/platform/menubar/node/menubar'; import { MenubarService } from 'vs/platform/menubar/electron-main/menubarService'; import { MenubarChannel } from 'vs/platform/menubar/node/menubarIpc'; import { hasArgs } from 'vs/platform/environment/node/argv'; @@ -65,8 +63,6 @@ import { homedir } from 'os'; import { join, sep } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; -import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; -import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService'; import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc'; @@ -74,15 +70,11 @@ import { startsWith } from 'vs/base/common/strings'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IBackupMainService } from 'vs/platform/backup/common/backup'; import { HistoryMainService } from 'vs/platform/history/electron-main/historyMainService'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; -import { VSBuffer } from 'vs/base/common/buffer'; import { statSync } from 'fs'; -import { ISignService } from 'vs/platform/sign/common/sign'; -import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnosticsService'; import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsIpc'; +import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -103,8 +95,7 @@ export class CodeApplication extends Disposable { @IEnvironmentService private readonly environmentService: IEnvironmentService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IStateService private readonly stateService: IStateService, - @ISignService private readonly signService: ISignService + @IStateService private readonly stateService: IStateService ) { super(); @@ -167,11 +158,13 @@ export class CodeApplication extends Disposable { event.preventDefault(); }); app.on('remote-get-current-web-contents', event => { - // The driver needs access to web contents - if (!this.environmentService.args.driver) { - this.logService.trace(`App#on(remote-get-current-web-contents): prevented`); - event.preventDefault(); + if (this.environmentService.args.driver) { + return; // the driver needs access to web contents } + + this.logService.trace(`App#on(remote-get-current-web-contents): prevented`); + + event.preventDefault(); }); app.on('web-contents-created', (_event: Electron.Event, contents) => { contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => { @@ -567,7 +560,7 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('url', urlChannel); const storageMainService = accessor.get(IStorageMainService); - const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService as StorageMainService)); + const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService)); electronIpcServer.registerChannel('storage', storageChannel); // Log level management @@ -695,112 +688,11 @@ export class CodeApplication extends Disposable { } private handleRemoteAuthorities(): void { - const connectionPool: Map = new Map(); - - class ActiveConnection { - private readonly _authority: string; - private readonly _connection: Promise; - private readonly _disposeRunner: RunOnceScheduler; - - constructor(authority: string, host: string, port: number, signService: ISignService) { - this._authority = authority; - - const options: IConnectionOptions = { - commit: product.commit, - socketFactory: nodeSocketFactory, - addressProvider: { - getAddress: () => { - return Promise.resolve({ host, port }); - } - }, - signService - }; - - this._connection = connectRemoteAgentManagement(options, authority, `main`); - this._disposeRunner = new RunOnceScheduler(() => this.dispose(), 5000); - } - - dispose(): void { - this._disposeRunner.dispose(); - connectionPool.delete(this._authority); - this._connection.then(connection => connection.dispose()); - } - - async getClient(): Promise> { - this._disposeRunner.schedule(); - const connection = await this._connection; - - return connection.client; - } - } - - const resolvedAuthorities = new Map(); - ipc.on('vscode:remoteAuthorityResolved', (event: Electron.Event, data: ResolvedAuthority) => { - this.logService.info('Received resolved authority', data.authority); - - resolvedAuthorities.set(data.authority, data); - - // Make sure to close and remove any existing connections - if (connectionPool.has(data.authority)) { - connectionPool.get(data.authority)!.dispose(); - } - }); - - const resolveAuthority = (authority: string): ResolvedAuthority | null => { - this.logService.info('Resolving authority', authority); - - if (authority.indexOf('+') >= 0) { - if (resolvedAuthorities.has(authority)) { - return withUndefinedAsNull(resolvedAuthorities.get(authority)); - } - - this.logService.info('Didnot find resolved authority for', authority); - - return null; - } else { - const [host, strPort] = authority.split(':'); - const port = parseInt(strPort, 10); - - return { authority, host, port }; - } - }; - - protocol.registerBufferProtocol(Schemas.vscodeRemote, async (request, callback) => { - if (request.method !== 'GET') { - return callback(undefined); - } - - const uri = URI.parse(request.url); - - let activeConnection: ActiveConnection | undefined; - if (connectionPool.has(uri.authority)) { - activeConnection = connectionPool.get(uri.authority); - } else { - const resolvedAuthority = resolveAuthority(uri.authority); - if (!resolvedAuthority) { - callback(undefined); - return; - } - - activeConnection = new ActiveConnection(uri.authority, resolvedAuthority.host, resolvedAuthority.port, this.signService); - connectionPool.set(uri.authority, activeConnection); - } - - try { - const rawClient = await activeConnection!.getClient(); - if (connectionPool.has(uri.authority)) { // not disposed in the meantime - const channel = rawClient.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); - - // TODO@alex don't use call directly, wrap it around a `RemoteExtensionsFileSystemProvider` - const fileContents = await channel.call('readFile', [uri]); - callback(fileContents.buffer); - } else { - callback(undefined); - } - } catch (err) { - onUnexpectedError(err); - callback(undefined); - } + protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { + callback({ + url: request.url.replace(/^vscode-remote-resource:/, 'http:'), + method: request.method + }); }); } } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index d9ed94ff68e50..32005dd153ea0 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -47,7 +47,6 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/product'; -import { ProductService } from 'vs/platform/product/node/productService'; const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); @@ -325,7 +324,7 @@ export async function main(argv: ParsedArgs): Promise { services.set(ILogService, logService); services.set(IConfigurationService, configurationService); services.set(IStateService, new SyncDescriptor(StateService)); - services.set(IProductService, new SyncDescriptor(ProductService)); + services.set(IProductService, { _serviceBrand: undefined, ...product }); // Files const fileService = new FileService(logService); diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 06d4b2f5307ad..418e8ebee98ea 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -49,7 +49,7 @@ export interface IPointerHandlerHelper { */ getLastViewCursorsRenderData(): IViewCursorRenderData[]; - shouldSuppressMouseDownOnViewZone(viewZoneId: number): boolean; + shouldSuppressMouseDownOnViewZone(viewZoneId: string): boolean; shouldSuppressMouseDownOnWidget(widgetId: string): boolean; /** diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 1f4c6ff52c726..f510e0f6f82ad 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -19,7 +19,7 @@ import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; export interface IViewZoneData { - viewZoneId: number; + viewZoneId: string; positionBefore: Position | null; positionAfter: Position | null; position: Position; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 496e9cae7ab45..a5e31344d3c2d 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -13,7 +13,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; @@ -83,17 +83,17 @@ export interface IViewZoneChangeAccessor { * @param zone Zone to create * @return A unique identifier to the view zone. */ - addZone(zone: IViewZone): number; + addZone(zone: IViewZone): string; /** * Remove a zone * @param id A unique identifier to the view zone, as returned by the `addZone` call. */ - removeZone(id: number): void; + removeZone(id: string): void; /** * Change a zone's position. * The editor will rescan the `afterLineNumber` and `afterColumn` properties of a view zone. */ - layoutZone(id: number): void; + layoutZone(id: string): void; } /** @@ -399,7 +399,7 @@ export interface ICodeEditor extends editorCommon.IEditor { */ onWillType(listener: (text: string) => void): IDisposable; /** - * An event emitted before interpreting typed characters (on the keyboard). + * An event emitted after interpreting typed characters (on the keyboard). * @event * @internal */ @@ -612,7 +612,7 @@ export interface ICodeEditor extends editorCommon.IEditor { * @param edits The edits to execute. * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; /** * Execute multiple (concomitant) commands on the editor. diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 106c75b5d046a..c7cf1da3f4535 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -229,11 +229,11 @@ const _CSS_MAP: { [prop: string]: string; } = { cursor: 'cursor:{0};', letterSpacing: 'letter-spacing:{0};', - gutterIconPath: 'background:url(\'{0}\') center center no-repeat;', + gutterIconPath: 'background:{0} center center no-repeat;', gutterIconSize: 'background-size:{0};', contentText: 'content:\'{0}\';', - contentIconPath: 'content:url(\'{0}\');', + contentIconPath: 'content:{0};', margin: 'margin:{0};', width: 'width:{0};', height: 'height:{0};' @@ -399,7 +399,7 @@ class DecorationCSSRules { if (typeof opts !== 'undefined') { this.collectBorderSettingsCSSText(opts, cssTextArr); if (typeof opts.contentIconPath !== 'undefined') { - cssTextArr.push(strings.format(_CSS_MAP.contentIconPath, dom.asDomUri(URI.revive(opts.contentIconPath)).toString(true).replace(/'/g, '%27'))); + cssTextArr.push(strings.format(_CSS_MAP.contentIconPath, dom.asCSSUrl(URI.revive(opts.contentIconPath)))); } if (typeof opts.contentText === 'string') { const truncated = opts.contentText.match(/^.*$/m)![0]; // only take first line @@ -426,7 +426,7 @@ class DecorationCSSRules { const cssTextArr: string[] = []; if (typeof opts.gutterIconPath !== 'undefined') { - cssTextArr.push(strings.format(_CSS_MAP.gutterIconPath, dom.asDomUri(URI.revive(opts.gutterIconPath)).toString(true).replace(/'/g, '%27'))); + cssTextArr.push(strings.format(_CSS_MAP.gutterIconPath, dom.asCSSUrl(URI.revive(opts.gutterIconPath)))); if (typeof opts.gutterIconSize !== 'undefined') { cssTextArr.push(strings.format(_CSS_MAP.gutterIconSize, opts.gutterIconSize)); } diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index c175034f9699f..07056377d38c9 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,42 +4,57 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpenerService, IOpener } from 'vs/platform/opener/common/opener'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { LinkedList } from 'vs/base/common/linkedList'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IOpener, IOpenerService, IValidator } from 'vs/platform/opener/common/opener'; -export class OpenerService implements IOpenerService { +export class OpenerService extends Disposable implements IOpenerService { - _serviceBrand: any; + _serviceBrand!: ServiceIdentifier; - private readonly _opener = new LinkedList(); + private readonly _openers = new LinkedList(); + private readonly _validators = new LinkedList(); constructor( @ICodeEditorService private readonly _editorService: ICodeEditorService, @ICommandService private readonly _commandService: ICommandService, ) { - // + super(); } registerOpener(opener: IOpener): IDisposable { - const remove = this._opener.push(opener); + const remove = this._openers.push(opener); return { dispose: remove }; } - async open(resource: URI, options?: { openToSide?: boolean }): Promise { + registerValidator(validator: IValidator): IDisposable { + const remove = this._validators.push(validator); + return { dispose: remove }; + } + + async open(resource: URI, options?: { openToSide?: boolean, openExternal?: boolean }): Promise { // no scheme ?!? if (!resource.scheme) { return Promise.resolve(false); } + + // check with contributed validators + for (const validator of this._validators.toArray()) { + if (!(await validator.shouldOpen(resource))) { + return false; + } + } + // check with contributed openers - for (const opener of this._opener.toArray()) { + for (const opener of this._openers.toArray()) { const handled = await opener.open(resource, options); if (handled) { return true; @@ -49,15 +64,18 @@ export class OpenerService implements IOpenerService { return this._doOpen(resource, options); } - private _doOpen(resource: URI, options?: { openToSide?: boolean }): Promise { + private _doOpen(resource: URI, options?: { openToSide?: boolean, openExternal?: boolean }): Promise { const { scheme, path, query, fragment } = resource; - if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https) || equalsIgnoreCase(scheme, Schemas.mailto)) { - // open http or default mail application - dom.windowOpenNoOpener(encodeURI(resource.toString(true))); - return Promise.resolve(true); + if (equalsIgnoreCase(scheme, Schemas.mailto) || (options && options.openExternal)) { + // open default mail application + return this._doOpenExternal(resource); + } + if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) { + // open link in default browser + return this._doOpenExternal(resource); } else if (equalsIgnoreCase(scheme, Schemas.command)) { // run command or bail out if command isn't known if (!CommandsRegistry.getCommand(path)) { @@ -100,4 +118,14 @@ export class OpenerService implements IOpenerService { ).then(() => true); } } + + private _doOpenExternal(resource: URI): Promise { + dom.windowOpenNoOpener(encodeURI(resource.toString(true))); + + return Promise.resolve(true); + } + + dispose() { + this._validators.clear(); + } } diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index a476c33912a4c..45c711ceceff8 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -248,7 +248,7 @@ export class View extends ViewEventHandler { getLastViewCursorsRenderData: () => { return this.viewCursors.getLastRenderData() || []; }, - shouldSuppressMouseDownOnViewZone: (viewZoneId: number) => { + shouldSuppressMouseDownOnViewZone: (viewZoneId: string) => { return this.viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); }, shouldSuppressMouseDownOnWidget: (widgetId: string) => { @@ -473,17 +473,17 @@ export class View extends ViewEventHandler { this._renderOnce(() => { const changeAccessor: editorBrowser.IViewZoneChangeAccessor = { - addZone: (zone: editorBrowser.IViewZone): number => { + addZone: (zone: editorBrowser.IViewZone): string => { zonesHaveChanged = true; return this.viewZones.addZone(zone); }, - removeZone: (id: number): void => { + removeZone: (id: string): void => { if (!id) { return; } zonesHaveChanged = this.viewZones.removeZone(id) || zonesHaveChanged; }, - layoutZone: (id: number): void => { + layoutZone: (id: string): void => { if (!id) { return; } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index c883223ae3175..1182f2ee8ab98 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -742,10 +742,6 @@ export class Minimap extends ViewPart { canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight); - // If the minimap is rendered using blocks, text takes up half the line height - const lineHeightRatio = renderMinimap === RenderMinimap.LargeBlocks || renderMinimap === RenderMinimap.SmallBlocks ? 0.5 : 1; - const height = lineHeight * lineHeightRatio; - // Loop over decorations, ignoring those that don't have the minimap property set and rendering rectangles for each line the decoration spans const lineOffsetMap = new Map(); for (let i = 0; i < decorations.length; i++) { @@ -756,7 +752,7 @@ export class Minimap extends ViewPart { } for (let line = decoration.range.startLineNumber; line <= decoration.range.endLineNumber; line++) { - this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration, layout, line, height, lineHeight, tabSize, characterWidth); + this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration, layout, line, lineHeight, lineHeight, tabSize, characterWidth); } } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index a51fbe5bf0285..6c01df4e0b6fc 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -14,7 +14,7 @@ import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewWhitespaceViewportData } from 'vs/editor/common/viewModel/viewModel'; export interface IMyViewZone { - whitespaceId: number; + whitespaceId: string; delegate: IViewZone; isVisible: boolean; domNode: FastDomNode; @@ -74,7 +74,7 @@ export class ViewZones extends ViewPart { const id = keys[i]; const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); - if (this._context.viewLayout.changeWhitespace(parseInt(id, 10), props.afterViewLineNumber, props.heightInPx)) { + if (this._context.viewLayout.changeWhitespace(id, props.afterViewLineNumber, props.heightInPx)) { this._safeCallOnComputedHeight(zone.delegate, props.heightInPx); hadAChange = true; } @@ -183,7 +183,7 @@ export class ViewZones extends ViewPart { }; } - public addZone(zone: IViewZone): number { + public addZone(zone: IViewZone): string { const props = this._computeWhitespaceProps(zone); const whitespaceId = this._context.viewLayout.addWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx, props.minWidthInPx); @@ -200,18 +200,18 @@ export class ViewZones extends ViewPart { myZone.domNode.setPosition('absolute'); myZone.domNode.domNode.style.width = '100%'; myZone.domNode.setDisplay('none'); - myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString()); + myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId); this.domNode.appendChild(myZone.domNode); if (myZone.marginDomNode) { myZone.marginDomNode.setPosition('absolute'); myZone.marginDomNode.domNode.style.width = '100%'; myZone.marginDomNode.setDisplay('none'); - myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString()); + myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId); this.marginDomNode.appendChild(myZone.marginDomNode); } - this._zones[myZone.whitespaceId.toString()] = myZone; + this._zones[myZone.whitespaceId] = myZone; this.setShouldRender(); @@ -219,10 +219,10 @@ export class ViewZones extends ViewPart { return myZone.whitespaceId; } - public removeZone(id: number): boolean { - if (this._zones.hasOwnProperty(id.toString())) { - const zone = this._zones[id.toString()]; - delete this._zones[id.toString()]; + public removeZone(id: string): boolean { + if (this._zones.hasOwnProperty(id)) { + const zone = this._zones[id]; + delete this._zones[id]; this._context.viewLayout.removeWhitespace(zone.whitespaceId); zone.domNode.removeAttribute('monaco-visible-view-zone'); @@ -242,10 +242,10 @@ export class ViewZones extends ViewPart { return false; } - public layoutZone(id: number): boolean { + public layoutZone(id: string): boolean { let changed = false; - if (this._zones.hasOwnProperty(id.toString())) { - const zone = this._zones[id.toString()]; + if (this._zones.hasOwnProperty(id)) { + const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); // const newOrdinal = this._getZoneOrdinal(zone.delegate); changed = this._context.viewLayout.changeWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx) || changed; @@ -259,9 +259,9 @@ export class ViewZones extends ViewPart { return changed; } - public shouldSuppressMouseDownOnViewZone(id: number): boolean { - if (this._zones.hasOwnProperty(id.toString())) { - const zone = this._zones[id.toString()]; + public shouldSuppressMouseDownOnViewZone(id: string): boolean { + if (this._zones.hasOwnProperty(id)) { + const zone = this._zones[id]; return Boolean(zone.delegate.suppressMouseDown); } return false; @@ -314,7 +314,7 @@ export class ViewZones extends ViewPart { let hasVisibleZone = false; for (let i = 0, len = visibleWhitespaces.length; i < len; i++) { - visibleZones[visibleWhitespaces[i].id.toString()] = visibleWhitespaces[i]; + visibleZones[visibleWhitespaces[i].id] = visibleWhitespaces[i]; hasVisibleZone = true; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e6a4fd0597778..0ac879ee78a59 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -33,7 +33,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; @@ -980,7 +980,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return true; } - public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean { + public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean { if (!this._modelData) { return false; } @@ -989,14 +989,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return false; } - this._modelData.model.pushEditOperations(this._modelData.cursor.getSelections(), edits, () => { - return endCursorState ? endCursorState : null; - }); - - if (endCursorState) { - this._modelData.cursor.setSelections(source, endCursorState); + let cursorStateComputer: ICursorStateComputer; + if (!endCursorState) { + cursorStateComputer = () => null; + } else if (Array.isArray(endCursorState)) { + cursorStateComputer = () => endCursorState; + } else { + cursorStateComputer = endCursorState; } + this._modelData.cursor.executeEdits(source, edits, cursorStateComputer); return true; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 260f479df551e..519cc25cbfd10 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -40,6 +40,9 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { INotificationService } from 'vs/platform/notification/common/notification'; import { defaultInsertColor, defaultRemoveColor, diffBorder, diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, getThemeTypeSelector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IDiffLinesChange, InlineDiffMargin } from 'vs/editor/browser/widget/inlineDiffMargin'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; interface IEditorDiffDecorations { decorations: IModelDeltaDecoration[]; @@ -47,7 +50,7 @@ interface IEditorDiffDecorations { } interface IEditorDiffDecorationsWithZones extends IEditorDiffDecorations { - zones: editorBrowser.IViewZone[]; + zones: IMyViewZone[]; } interface IEditorsDiffDecorationsWithZones { @@ -56,8 +59,8 @@ interface IEditorsDiffDecorationsWithZones { } interface IEditorsZones { - original: editorBrowser.IViewZone[]; - modified: editorBrowser.IViewZone[]; + original: IMyViewZone[]; + modified: IMyViewZone[]; } interface IDiffEditorWidgetStyle { @@ -69,12 +72,17 @@ interface IDiffEditorWidgetStyle { } class VisualEditorState { - private _zones: number[]; + private _zones: string[]; + private inlineDiffMargins: InlineDiffMargin[]; private _zonesMap: { [zoneId: string]: boolean; }; private _decorations: string[]; - constructor() { + constructor( + private _contextMenuService: IContextMenuService, + private _clipboardService: IClipboardService + ) { this._zones = []; + this.inlineDiffMargins = []; this._zonesMap = {}; this._decorations = []; } @@ -108,13 +116,22 @@ class VisualEditorState { for (let i = 0, length = this._zones.length; i < length; i++) { viewChangeAccessor.removeZone(this._zones[i]); } + for (let i = 0, length = this.inlineDiffMargins.length; i < length; i++) { + this.inlineDiffMargins[i].dispose(); + } this._zones = []; this._zonesMap = {}; + this.inlineDiffMargins = []; for (let i = 0, length = newDecorations.zones.length; i < length; i++) { - newDecorations.zones[i].suppressMouseDown = true; - let zoneId = viewChangeAccessor.addZone(newDecorations.zones[i]); + const viewZone = newDecorations.zones[i]; + viewZone.suppressMouseDown = false; + let zoneId = viewChangeAccessor.addZone(viewZone); this._zones.push(zoneId); this._zonesMap[String(zoneId)] = true; + + if (newDecorations.zones[i].diff && viewZone.marginDomNode) { + this.inlineDiffMargins.push(new InlineDiffMargin(viewZone.marginDomNode, editor, newDecorations.zones[i].diff!, this._contextMenuService, this._clipboardService)); + } } }); @@ -202,7 +219,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @IThemeService themeService: IThemeService, - @INotificationService notificationService: INotificationService + @INotificationService notificationService: INotificationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IClipboardService clipboardService: IClipboardService ) { super(); @@ -282,8 +301,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._currentlyChangingViewZones = false; this._diffComputationToken = 0; - this._originalEditorState = new VisualEditorState(); - this._modifiedEditorState = new VisualEditorState(); + this._originalEditorState = new VisualEditorState(contextMenuService, clipboardService); + this._modifiedEditorState = new VisualEditorState(contextMenuService, clipboardService); this._isVisible = true; this._isHandlingScrollEvent = false; @@ -1258,6 +1277,7 @@ interface IMyViewZone { minWidthInPx?: number; domNode: HTMLElement | null; marginDomNode?: HTMLElement | null; + diff?: IDiffLinesChange; } class ForeignViewZonesIterator { @@ -1469,12 +1489,12 @@ abstract class ViewZonesComputer { }; } - private static _ensureDomNodes(zones: IMyViewZone[]): editorBrowser.IViewZone[] { + private static _ensureDomNodes(zones: IMyViewZone[]): IMyViewZone[] { return zones.map((z) => { if (!z.domNode) { z.domNode = createFakeLinesDiv(); } - return z; + return z; }); } @@ -1977,8 +1997,10 @@ class InlineViewZonesComputer extends ViewZonesComputer { let lineHeight = this.modifiedEditorConfiguration.lineHeight; const typicalHalfwidthCharacterWidth = this.modifiedEditorConfiguration.fontInfo.typicalHalfwidthCharacterWidth; let maxCharsPerLine = 0; + const originalContent: string[] = []; for (let lineNumber = lineChange.originalStartLineNumber; lineNumber <= lineChange.originalEndLineNumber; lineNumber++) { maxCharsPerLine = Math.max(maxCharsPerLine, this._renderOriginalLine(lineNumber - lineChange.originalStartLineNumber, this.originalModel, this.modifiedEditorConfiguration, this.modifiedEditorTabSize, lineNumber, decorations, sb)); + originalContent.push(this.originalModel.getLineContent(lineNumber)); if (this.renderIndicators) { let index = lineNumber - lineChange.originalStartLineNumber; @@ -2005,7 +2027,14 @@ class InlineViewZonesComputer extends ViewZonesComputer { heightInLines: lineChangeOriginalLength, minWidthInPx: (maxCharsPerLine * typicalHalfwidthCharacterWidth), domNode: domNode, - marginDomNode: marginDomNode + marginDomNode: marginDomNode, + diff: { + originalStartLineNumber: lineChange.originalStartLineNumber, + originalEndLineNumber: lineChange.originalEndLineNumber, + modifiedStartLineNumber: lineChange.modifiedStartLineNumber, + modifiedEndLineNumber: lineChange.modifiedEndLineNumber, + originalContent: originalContent + } }; } diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 1c6f251a3edda..2f36d8e97656d 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -16,6 +16,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @@ -74,9 +76,11 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @IThemeService themeService: IThemeService, - @INotificationService notificationService: INotificationService + @INotificationService notificationService: INotificationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IClipboardService clipboardService: IClipboardService ) { - super(domElement, parentEditor.getRawConfiguration(), editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService); + super(domElement, parentEditor.getRawConfiguration(), editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, clipboardService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/browser/widget/inlineDiffMargin.ts b/src/vs/editor/browser/widget/inlineDiffMargin.ts new file mode 100644 index 0000000000000..2bca61666b42e --- /dev/null +++ b/src/vs/editor/browser/widget/inlineDiffMargin.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. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { Action } from 'vs/base/common/actions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { Range } from 'vs/editor/common/core/range'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; + +export interface IDiffLinesChange { + readonly originalStartLineNumber: number; + readonly originalEndLineNumber: number; + readonly modifiedStartLineNumber: number; + readonly modifiedEndLineNumber: number; + readonly originalContent: string[]; +} + +export class InlineDiffMargin extends Disposable { + private readonly _lightBulb: HTMLElement; + + constructor( + marginDomNode: HTMLElement, + public editor: CodeEditorWidget, + public diff: IDiffLinesChange, + private _contextMenuService: IContextMenuService, + private _clipboardService: IClipboardService + ) { + super(); + + // make sure the diff margin shows above overlay. + marginDomNode.style.zIndex = '10'; + + this._lightBulb = document.createElement('div'); + this._lightBulb.className = 'lightbulb-glyph'; + this._lightBulb.style.position = 'absolute'; + const lineHeight = editor.getConfiguration().lineHeight; + const lineFeed = editor.getModel()!.getEOL(); + this._lightBulb.style.right = '0px'; + this._lightBulb.style.visibility = 'hidden'; + this._lightBulb.style.height = `${lineHeight}px`; + marginDomNode.appendChild(this._lightBulb); + + const actions = [ + new Action( + 'diff.clipboard.copyDeletedContent', + nls.localize('diff.clipboard.copyDeletedContent.label', "Copy deleted lines content to clipboard"), + undefined, + true, + async () => { + await this._clipboardService.writeText(diff.originalContent.join(lineFeed) + lineFeed); + } + ) + ]; + + let currentLineNumberOffset = 0; + + const copyLineAction = new Action( + 'diff.clipboard.copyDeletedLineContent', + nls.localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line {0} content to clipboard", diff.originalStartLineNumber), + undefined, + true, + async () => { + await this._clipboardService.writeText(diff.originalContent[currentLineNumberOffset]); + } + ); + + actions.push(copyLineAction); + + const readOnly = editor.getConfiguration().readOnly; + if (!readOnly) { + actions.push(new Action('diff.inline.revertChange', nls.localize('diff.inline.revertChange.label', "Revert this change"), undefined, true, async () => { + if (diff.modifiedEndLineNumber === 0) { + // deletion only + const column = editor.getModel()!.getLineMaxColumn(diff.modifiedStartLineNumber); + editor.executeEdits('diffEditor', [ + { + range: new Range(diff.modifiedStartLineNumber, column, diff.modifiedStartLineNumber, column), + text: lineFeed + diff.originalContent.join(lineFeed) + } + ]); + } else { + const column = editor.getModel()!.getLineMaxColumn(diff.modifiedEndLineNumber); + editor.executeEdits('diffEditor', [ + { + range: new Range(diff.modifiedStartLineNumber, 1, diff.modifiedEndLineNumber, column), + text: diff.originalContent.join(lineFeed) + } + ]); + } + + })); + } + + this._register(dom.addStandardDisposableListener(marginDomNode, 'mouseenter', e => { + this._lightBulb.style.visibility = 'visible'; + currentLineNumberOffset = this._updateLightBulbPosition(marginDomNode, e.y, lineHeight); + })); + + this._register(dom.addStandardDisposableListener(marginDomNode, 'mouseleave', e => { + this._lightBulb.style.visibility = 'hidden'; + })); + + this._register(dom.addStandardDisposableListener(marginDomNode, 'mousemove', e => { + currentLineNumberOffset = this._updateLightBulbPosition(marginDomNode, e.y, lineHeight); + })); + + this._register(dom.addStandardDisposableListener(this._lightBulb, 'mousedown', e => { + const { top, height } = dom.getDomNodePagePosition(this._lightBulb); + let pad = Math.floor(lineHeight / 3) + lineHeight; + this._contextMenuService.showContextMenu({ + getAnchor: () => { + return { + x: e.posx, + y: top + height + pad + }; + }, + getActions: () => { + copyLineAction.label = nls.localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line {0} content to clipboard", diff.originalStartLineNumber + currentLineNumberOffset); + return actions; + }, + autoSelectFirstItem: true + }); + })); + } + + private _updateLightBulbPosition(marginDomNode: HTMLElement, y: number, lineHeight: number): number { + const { top } = dom.getDomNodePagePosition(marginDomNode); + const offset = y - top; + const lineNumberOffset = Math.floor(offset / lineHeight); + const newTop = lineNumberOffset * lineHeight; + this._lightBulb.style.top = `${newTop}px`; + return lineNumberOffset; + } +} diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 71b0cbb884391..997d22be61459 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2185,7 +2185,7 @@ export class InternalEditorOptionsFactory { selectOnLineNumbers: opts.viewInfo.selectOnLineNumbers, glyphMargin: opts.viewInfo.glyphMargin, revealHorizontalRightPadding: opts.viewInfo.revealHorizontalRightPadding, - roundedSelection: (accessibilityIsOn ? false : opts.viewInfo.roundedSelection), // DISABLED WHEN SCREEN READER IS ATTACHED + roundedSelection: opts.viewInfo.roundedSelection, overviewRulerLanes: opts.viewInfo.overviewRulerLanes, overviewRulerBorder: opts.viewInfo.overviewRulerBorder, cursorBlinking: opts.viewInfo.cursorBlinking, @@ -2198,15 +2198,15 @@ export class InternalEditorOptionsFactory { scrollBeyondLastColumn: opts.viewInfo.scrollBeyondLastColumn, smoothScrolling: opts.viewInfo.smoothScrolling, stopRenderingLineAfter: opts.viewInfo.stopRenderingLineAfter, - renderWhitespace: (accessibilityIsOn ? 'none' : opts.viewInfo.renderWhitespace), // DISABLED WHEN SCREEN READER IS ATTACHED - renderControlCharacters: (accessibilityIsOn ? false : opts.viewInfo.renderControlCharacters), // DISABLED WHEN SCREEN READER IS ATTACHED + renderWhitespace: opts.viewInfo.renderWhitespace, + renderControlCharacters: opts.viewInfo.renderControlCharacters, fontLigatures: opts.viewInfo.fontLigatures, - renderIndentGuides: (accessibilityIsOn ? false : opts.viewInfo.renderIndentGuides), // DISABLED WHEN SCREEN READER IS ATTACHED + renderIndentGuides: opts.viewInfo.renderIndentGuides, highlightActiveIndentGuide: opts.viewInfo.highlightActiveIndentGuide, renderLineHighlight: opts.viewInfo.renderLineHighlight, scrollbar: opts.viewInfo.scrollbar, minimap: { - enabled: (accessibilityIsOn ? false : opts.viewInfo.minimap.enabled), // DISABLED WHEN SCREEN READER IS ATTACHED + enabled: opts.viewInfo.minimap.enabled, side: opts.viewInfo.minimap.side, renderCharacters: opts.viewInfo.minimap.renderCharacters, showSlider: opts.viewInfo.minimap.showSlider, @@ -2218,7 +2218,7 @@ export class InternalEditorOptionsFactory { contribInfo: { selectionClipboard: opts.contribInfo.selectionClipboard, hover: opts.contribInfo.hover, - links: (accessibilityIsOn ? false : opts.contribInfo.links), // DISABLED WHEN SCREEN READER IS ATTACHED + links: opts.contribInfo.links, contextmenu: opts.contribInfo.contextmenu, quickSuggestions: opts.contribInfo.quickSuggestions, quickSuggestionsDelay: opts.contribInfo.quickSuggestionsDelay, @@ -2235,13 +2235,13 @@ export class InternalEditorOptionsFactory { tabCompletion: opts.contribInfo.tabCompletion, suggest: opts.contribInfo.suggest, gotoLocation: opts.contribInfo.gotoLocation, - selectionHighlight: (accessibilityIsOn ? false : opts.contribInfo.selectionHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED - occurrencesHighlight: (accessibilityIsOn ? false : opts.contribInfo.occurrencesHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED - codeLens: (accessibilityIsOn ? false : opts.contribInfo.codeLens), // DISABLED WHEN SCREEN READER IS ATTACHED + selectionHighlight: opts.contribInfo.selectionHighlight, + occurrencesHighlight: opts.contribInfo.occurrencesHighlight, + codeLens: opts.contribInfo.codeLens, folding: (accessibilityIsOn ? false : opts.contribInfo.folding), // DISABLED WHEN SCREEN READER IS ATTACHED foldingStrategy: opts.contribInfo.foldingStrategy, showFoldingControls: opts.contribInfo.showFoldingControls, - matchBrackets: (accessibilityIsOn ? false : opts.contribInfo.matchBrackets), // DISABLED WHEN SCREEN READER IS ATTACHED + matchBrackets: opts.contribInfo.matchBrackets, find: opts.contribInfo.find, colorDecorators: opts.contribInfo.colorDecorators, lightbulbEnabled: opts.contribInfo.lightbulbEnabled, diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 789d3a97b021c..1b448239065ef 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer } from 'vs/editor/common/model'; import { RawContentChangedType } from 'vs/editor/common/model/textModelEvents'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; @@ -86,6 +86,14 @@ export class CursorModelState { class AutoClosedAction { + public static getAllAutoClosedCharacters(autoClosedActions: AutoClosedAction[]): Range[] { + let autoClosedCharacters: Range[] = []; + for (const autoClosedAction of autoClosedActions) { + autoClosedCharacters = autoClosedCharacters.concat(autoClosedAction.getAutoClosedCharactersRanges()); + } + return autoClosedCharacters; + } + private readonly _model: ITextModel; private _autoClosedCharactersDecorations: string[]; @@ -429,6 +437,31 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ------ auxiliary handling logic + private _pushAutoClosedAction(autoClosedCharactersRanges: Range[], autoClosedEnclosingRanges: Range[]): void { + let autoClosedCharactersDeltaDecorations: IModelDeltaDecoration[] = []; + let autoClosedEnclosingDeltaDecorations: IModelDeltaDecoration[] = []; + + for (let i = 0, len = autoClosedCharactersRanges.length; i < len; i++) { + autoClosedCharactersDeltaDecorations.push({ + range: autoClosedCharactersRanges[i], + options: { + inlineClassName: 'auto-closed-character', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + } + }); + autoClosedEnclosingDeltaDecorations.push({ + range: autoClosedEnclosingRanges[i], + options: { + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + } + }); + } + + const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersDeltaDecorations); + const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingDeltaDecorations); + this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations)); + } + private _executeEditOperation(opResult: EditOperationResult | null): void { if (!opResult) { @@ -446,32 +479,19 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._interpretCommandResult(result); // Check for auto-closing closed characters - let autoClosedCharactersRanges: IModelDeltaDecoration[] = []; - let autoClosedEnclosingRanges: IModelDeltaDecoration[] = []; + let autoClosedCharactersRanges: Range[] = []; + let autoClosedEnclosingRanges: Range[] = []; for (let i = 0; i < opResult.commands.length; i++) { const command = opResult.commands[i]; if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { - autoClosedCharactersRanges.push({ - range: command.closeCharacterRange, - options: { - inlineClassName: 'auto-closed-character', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - } - }); - autoClosedEnclosingRanges.push({ - range: command.enclosingRange, - options: { - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - } - }); + autoClosedCharactersRanges.push(command.closeCharacterRange); + autoClosedEnclosingRanges.push(command.enclosingRange); } } if (autoClosedCharactersRanges.length > 0) { - const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersRanges); - const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingRanges); - this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations)); + this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); } this._prevEditOperationType = opResult.type; @@ -563,6 +583,76 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ----------------------------------------------------------------------------------------------------------- // ----- handlers beyond this point + private _findAutoClosingPairs(edits: IIdentifiedSingleEditOperation[]): [number, number][] | null { + if (!edits.length) { + return null; + } + + let indices: [number, number][] = []; + for (let i = 0, len = edits.length; i < len; i++) { + const edit = edits[i]; + if (!edit.text || edit.text.indexOf('\n') >= 0) { + return null; + } + + const m = edit.text.match(/([)\]}>'"`])([^)\]}>'"`]*)$/); + if (!m) { + return null; + } + const closeChar = m[1]; + + const autoClosingPairsCandidates = this.context.config.autoClosingPairsClose2.get(closeChar); + if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) { + return null; + } + + const openChar = autoClosingPairsCandidates[0].open; + const closeCharIndex = edit.text.length - m[2].length - 1; + const openCharIndex = edit.text.lastIndexOf(openChar, closeCharIndex - 1); + if (openCharIndex === -1) { + return null; + } + + indices.push([openCharIndex, closeCharIndex]); + } + + return indices; + } + + public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { + let autoClosingIndices: [number, number][] | null = null; + if (source === 'snippet') { + autoClosingIndices = this._findAutoClosingPairs(edits); + } + + if (autoClosingIndices) { + edits[0]._isTracked = true; + } + let autoClosedCharactersRanges: Range[] = []; + let autoClosedEnclosingRanges: Range[] = []; + const selections = this._model.pushEditOperations(this.getSelections(), edits, (undoEdits) => { + if (autoClosingIndices) { + for (let i = 0, len = autoClosingIndices.length; i < len; i++) { + const [openCharInnerIndex, closeCharInnerIndex] = autoClosingIndices[i]; + const undoEdit = undoEdits[i]; + const lineNumber = undoEdit.range.startLineNumber; + const openCharIndex = undoEdit.range.startColumn - 1 + openCharInnerIndex; + const closeCharIndex = undoEdit.range.startColumn - 1 + closeCharInnerIndex; + + autoClosedCharactersRanges.push(new Range(lineNumber, closeCharIndex + 1, lineNumber, closeCharIndex + 2)); + autoClosedEnclosingRanges.push(new Range(lineNumber, openCharIndex + 1, lineNumber, closeCharIndex + 2)); + } + } + return cursorStateComputer(undoEdits); + }); + if (selections) { + this.setSelections(source, selections); + } + if (autoClosedCharactersRanges.length > 0) { + this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); + } + } + public trigger(source: string, handlerId: string, payload: any): void { const H = editorCommon.Handler; @@ -657,7 +747,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { private _interpretCompositionEnd(source: string) { if (!this._isDoingComposition && source === 'keyboard') { // composition finishes, let's check if we need to auto complete if necessary. - this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections())); + const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); + this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters)); } } @@ -675,14 +766,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { chr = text.charAt(i); } - let autoClosedCharacters: Range[] = []; - if (this._autoClosedActions.length > 0) { - for (let i = 0, len = this._autoClosedActions.length; i < len; i++) { - autoClosedCharacters = autoClosedCharacters.concat(this._autoClosedActions[i].getAutoClosedCharactersRanges()); - } - } - - // Here we must interpret each typed character individually, that's why we create a new context + // Here we must interpret each typed character individually + const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr)); } diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index 844387516d67d..f459e61735fe2 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -15,7 +15,7 @@ import { ICommand, IConfiguration, ScrollType } from 'vs/editor/common/editorCom import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration'; +import { IAutoClosingPair, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; @@ -67,11 +67,22 @@ export interface ICursors { export interface CharacterMap { [char: string]: string; } +export interface MultipleCharacterMap { + [char: string]: string[]; +} const autoCloseAlways = () => true; const autoCloseNever = () => false; const autoCloseBeforeWhitespace = (chr: string) => (chr === ' ' || chr === '\t'); +function appendEntry(target: Map, key: K, value: V): void { + if (target.has(key)) { + target.get(key)!.push(value); + } else { + target.set(key, [value]); + } +} + export class CursorConfiguration { _cursorMoveConfigurationBrand: void; @@ -90,8 +101,8 @@ export class CursorConfiguration { public readonly autoClosingQuotes: EditorAutoClosingStrategy; public readonly autoSurround: EditorAutoSurroundStrategy; public readonly autoIndent: boolean; - public readonly autoClosingPairsOpen: CharacterMap; - public readonly autoClosingPairsClose: CharacterMap; + public readonly autoClosingPairsOpen2: Map; + public readonly autoClosingPairsClose2: Map; public readonly surroundingPairs: CharacterMap; public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean, bracket: (ch: string) => boolean }; @@ -138,8 +149,8 @@ export class CursorConfiguration { this.autoSurround = c.autoSurround; this.autoIndent = c.autoIndent; - this.autoClosingPairsOpen = {}; - this.autoClosingPairsClose = {}; + this.autoClosingPairsOpen2 = new Map(); + this.autoClosingPairsClose2 = new Map(); this.surroundingPairs = {}; this._electricChars = null; @@ -151,8 +162,10 @@ export class CursorConfiguration { let autoClosingPairs = CursorConfiguration._getAutoClosingPairs(languageIdentifier); if (autoClosingPairs) { for (const pair of autoClosingPairs) { - this.autoClosingPairsOpen[pair.open] = pair.close; - this.autoClosingPairsClose[pair.close] = pair.open; + appendEntry(this.autoClosingPairsOpen2, pair.open.charAt(pair.open.length - 1), pair); + if (pair.close.length === 1) { + appendEntry(this.autoClosingPairsClose2, pair.close, pair); + } } } @@ -190,7 +203,7 @@ export class CursorConfiguration { } } - private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] | null { + private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): StandardAutoClosingPairConditional[] | null { try { return LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id); } catch (e) { diff --git a/src/vs/editor/common/controller/cursorDeleteOperations.ts b/src/vs/editor/common/controller/cursorDeleteOperations.ts index 5a7830e26b3b3..3f5e80a3ee96b 100644 --- a/src/vs/editor/common/controller/cursorDeleteOperations.ts +++ b/src/vs/editor/common/controller/cursorDeleteOperations.ts @@ -63,7 +63,8 @@ export class DeleteOperations { const lineText = model.getLineContent(position.lineNumber); const character = lineText[position.column - 2]; - if (!config.autoClosingPairsOpen.hasOwnProperty(character)) { + const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(character); + if (!autoClosingPairCandidates) { return false; } @@ -78,9 +79,14 @@ export class DeleteOperations { } const afterCharacter = lineText[position.column - 1]; - const closeCharacter = config.autoClosingPairsOpen[character]; - if (afterCharacter !== closeCharacter) { + let foundAutoClosingPair = false; + for (const autoClosingPairCandidate of autoClosingPairCandidates) { + if (autoClosingPairCandidate.open === character && autoClosingPairCandidate.close === afterCharacter) { + foundAutoClosingPair = true; + } + } + if (!foundAutoClosingPair) { return false; } } diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 7ea1d26dffdfa..aa6b574742040 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -13,9 +13,10 @@ import { CursorColumns, CursorConfiguration, EditOperationResult, EditOperationT import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { Position } from 'vs/editor/common/core/position'; import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { EnterAction, IndentAction } from 'vs/editor/common/modes/languageConfiguration'; +import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter'; @@ -433,7 +434,11 @@ export class TypeOperations { private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { const autoCloseConfig = isQuote(ch) ? config.autoClosingQuotes : config.autoClosingBrackets; - if (autoCloseConfig === 'never' || !config.autoClosingPairsClose.hasOwnProperty(ch)) { + if (autoCloseConfig === 'never') { + return false; + } + + if (!config.autoClosingPairsClose2.has(ch)) { return false; } @@ -469,15 +474,6 @@ export class TypeOperations { return true; } - private static _countNeedlesInHaystack(haystack: string, needle: string): number { - let cnt = 0; - let lastIndex = -1; - while ((lastIndex = haystack.indexOf(needle, lastIndex + 1)) !== -1) { - cnt++; - } - return cnt; - } - private static _runAutoClosingCloseCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { let commands: ICommand[] = []; for (let i = 0, len = selections.length; i < len; i++) { @@ -492,65 +488,98 @@ export class TypeOperations { }); } - private static _isBeforeClosingBrace(config: CursorConfiguration, ch: string, characterAfter: string) { - const thisBraceIsSymmetric = (config.autoClosingPairsOpen[ch] === ch); - let isBeforeCloseBrace = false; - for (let otherCloseBrace in config.autoClosingPairsClose) { - const otherBraceIsSymmetric = (config.autoClosingPairsOpen[otherCloseBrace] === otherCloseBrace); + private static _isBeforeClosingBrace(config: CursorConfiguration, autoClosingPair: StandardAutoClosingPairConditional, characterAfter: string) { + const otherAutoClosingPairs = config.autoClosingPairsClose2.get(characterAfter); + if (!otherAutoClosingPairs) { + return false; + } + + const thisBraceIsSymmetric = (autoClosingPair.open === autoClosingPair.close); + for (const otherAutoClosingPair of otherAutoClosingPairs) { + const otherBraceIsSymmetric = (otherAutoClosingPair.open === otherAutoClosingPair.close); if (!thisBraceIsSymmetric && otherBraceIsSymmetric) { continue; } - if (characterAfter === otherCloseBrace) { - isBeforeCloseBrace = true; - break; - } + return true; } - return isBeforeCloseBrace; + return false; } - private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { + private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { + const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(ch); + if (!autoClosingPairCandidates) { + return null; + } + + // Determine which auto-closing pair it is + let autoClosingPair: StandardAutoClosingPairConditional | null = null; + for (const autoClosingPairCandidate of autoClosingPairCandidates) { + if (autoClosingPair === null || autoClosingPairCandidate.open.length > autoClosingPair.open.length) { + let candidateIsMatch = true; + for (const position of positions) { + const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - autoClosingPairCandidate.open.length + 1, position.lineNumber, position.column)); + if (relevantText + ch !== autoClosingPairCandidate.open) { + candidateIsMatch = false; + break; + } + } + + if (candidateIsMatch) { + autoClosingPair = autoClosingPairCandidate; + } + } + } + return autoClosingPair; + } + + private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean): StandardAutoClosingPairConditional | null { const chIsQuote = isQuote(ch); const autoCloseConfig = chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets; + if (autoCloseConfig === 'never') { + return null; + } - if (autoCloseConfig === 'never' || !config.autoClosingPairsOpen.hasOwnProperty(ch)) { - return false; + const autoClosingPair = this._findAutoClosingPairOpen(config, model, selections.map(s => s.getPosition()), ch); + if (!autoClosingPair) { + return null; } - let shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket; + const shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket; for (let i = 0, len = selections.length; i < len; i++) { const selection = selections[i]; if (!selection.isEmpty()) { - return false; + return null; } const position = selection.getPosition(); const lineText = model.getLineContent(position.lineNumber); - // Do not auto-close ' or " after a word character - if ((chIsQuote && position.column > 1) && autoCloseConfig !== 'always') { - const wordSeparators = getMapForWordSeparators(config.wordSeparators); - const characterBeforeCode = lineText.charCodeAt(position.column - 2); - const characterBeforeType = wordSeparators.get(characterBeforeCode); - if (characterBeforeType === WordCharacterClass.Regular) { - return false; - } - } - // Only consider auto closing the pair if a space follows or if another autoclosed pair follows - const characterAfter = lineText.charAt(position.column - 1); - if (characterAfter) { - let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter); + if (lineText.length > position.column - 1) { + const characterAfter = lineText.charAt(position.column - 1); + const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, autoClosingPair, characterAfter); if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { - return false; + return null; } } if (!model.isCheapToTokenize(position.lineNumber)) { // Do not force tokenization - return false; + return null; + } + + // Do not auto-close ' or " after a word character + if (autoClosingPair.open.length === 1 && chIsQuote && autoCloseConfig !== 'always') { + const wordSeparators = getMapForWordSeparators(config.wordSeparators); + if (insertOpenCharacter && position.column > 1 && wordSeparators.get(lineText.charCodeAt(position.column - 2)) === WordCharacterClass.Regular) { + return null; + } + if (!insertOpenCharacter && position.column > 2 && wordSeparators.get(lineText.charCodeAt(position.column - 3)) === WordCharacterClass.Regular) { + return null; + } } model.forceTokenization(position.lineNumber); @@ -558,25 +587,24 @@ export class TypeOperations { let shouldAutoClosePair = false; try { - shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column); + shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(autoClosingPair, lineTokens, insertOpenCharacter ? position.column : position.column - 1); } catch (e) { onUnexpectedError(e); } if (!shouldAutoClosePair) { - return false; + return null; } } - return true; + return autoClosingPair; } - private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { + private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean, autoClosingPair: StandardAutoClosingPairConditional): EditOperationResult { let commands: ICommand[] = []; for (let i = 0, len = selections.length; i < len; i++) { const selection = selections[i]; - const closeCharacter = config.autoClosingPairsOpen[ch]; - commands[i] = new TypeWithAutoClosingCommand(selection, ch, closeCharacter); + commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPair.close); } return new EditOperationResult(EditOperationType.Typing, commands, { shouldPushStackElementBefore: true, @@ -679,14 +707,6 @@ export class TypeOperations { return null; } - if (electricAction.appendText) { - const command = new ReplaceCommandWithOffsetCursorState(selection, ch + electricAction.appendText, 0, -electricAction.appendText.length); - return new EditOperationResult(EditOperationType.Typing, [command], { - shouldPushStackElementBefore: false, - shouldPushStackElementAfter: true - }); - } - if (electricAction.matchOpenBracket) { let endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; let match = model.findMatchingBracketUp(electricAction.matchOpenBracket, { @@ -722,87 +742,44 @@ export class TypeOperations { return null; } - public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[]): EditOperationResult | null { - if (config.autoClosingQuotes === 'never') { - return null; - } - - let commands: ICommand[] = []; - - for (let i = 0; i < selections.length; i++) { - if (!selections[i].isEmpty()) { - continue; + /** + * This is very similar with typing, but the character is already in the text buffer! + */ + public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[]): EditOperationResult | null { + let ch: string | null = null; + // extract last typed character + for (const selection of selections) { + if (!selection.isEmpty()) { + return null; } - const position = selections[i].getPosition(); - const lineText = model.getLineContent(position.lineNumber); - const ch = lineText.charAt(position.column - 2); - - if (config.autoClosingPairsClose.hasOwnProperty(ch)) { // first of all, it's a closing tag - if (ch === config.autoClosingPairsClose[ch] /** isEqualPair */) { - const lineTextBeforeCursor = lineText.substr(0, position.column - 2); - const chCntBefore = this._countNeedlesInHaystack(lineTextBeforeCursor, ch); - - if (chCntBefore % 2 === 1) { - continue; // it pairs with the opening tag. - } - } + const position = selection.getPosition(); + const currentChar = model.getValueInRange(new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column)); + if (ch === null) { + ch = currentChar; + } else if (ch !== currentChar) { + return null; } + } - // As we are not typing in a new character, so we don't need to run `_runAutoClosingCloseCharType` - // Next step, let's try to check if it's an open char. - if (config.autoClosingPairsOpen.hasOwnProperty(ch)) { - if (isQuote(ch) && position.column > 2) { - const wordSeparators = getMapForWordSeparators(config.wordSeparators); - const characterBeforeCode = lineText.charCodeAt(position.column - 3); - const characterBeforeType = wordSeparators.get(characterBeforeCode); - if (characterBeforeType === WordCharacterClass.Regular) { - continue; - } - } - - const characterAfter = lineText.charAt(position.column - 1); - - if (characterAfter) { - let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter); - let shouldAutoCloseBefore = isQuote(ch) ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket; - if (isBeforeCloseBrace) { - // In normal auto closing logic, we will auto close if the cursor is even before a closing brace intentionally. - // However for composition mode, we do nothing here as users might clear all the characters for composition and we don't want to do a unnecessary auto close. - // Related: microsoft/vscode#57250. - continue; - } - if (!shouldAutoCloseBefore(characterAfter)) { - continue; - } - } - - if (!model.isCheapToTokenize(position.lineNumber)) { - // Do not force tokenization - continue; - } - - model.forceTokenization(position.lineNumber); - const lineTokens = model.getLineTokens(position.lineNumber); - - let shouldAutoClosePair = false; + if (!ch) { + return null; + } - try { - shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column - 1); - } catch (e) { - onUnexpectedError(e); - } + if (this._isAutoClosingCloseCharType(config, model, selections, autoClosedCharacters, ch)) { + // Unfortunately, the close character is at this point "doubled", so we need to delete it... + const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); + return new EditOperationResult(EditOperationType.Typing, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } - if (shouldAutoClosePair) { - const closeCharacter = config.autoClosingPairsOpen[ch]; - commands[i] = new ReplaceCommandWithOffsetCursorState(selections[i], closeCharacter, 0, -closeCharacter.length); - } - } + const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, false); + if (autoClosingPairOpenCharType) { + return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairOpenCharType); } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); + return null; } public static typeWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { @@ -840,8 +817,9 @@ export class TypeOperations { return this._runAutoClosingCloseCharType(prevEditOperationType, config, model, selections, ch); } - if (this._isAutoClosingOpenCharType(config, model, selections, ch)) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch); + const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, true); + if (autoClosingPairOpenCharType) { + return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairOpenCharType); } if (this._isSurroundSelectionType(config, model, selections, ch)) { @@ -929,12 +907,14 @@ export class TypeOperations { export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { - private _closeCharacter: string; + private readonly _openCharacter: string; + private readonly _closeCharacter: string; public closeCharacterRange: Range | null; public enclosingRange: Range | null; - constructor(selection: Selection, openCharacter: string, closeCharacter: string) { - super(selection, openCharacter + closeCharacter, 0, -closeCharacter.length); + constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { + super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length); + this._openCharacter = openCharacter; this._closeCharacter = closeCharacter; this.closeCharacterRange = null; this.enclosingRange = null; @@ -944,7 +924,7 @@ export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorSt let inverseEditOperations = helper.getInverseEditOperations(); let range = inverseEditOperations[0].range; this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); - this.enclosingRange = range; + this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); return super.computeCursorState(model, helper); } } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 874d7428cf3c4..0469471f8c4c4 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -110,21 +110,6 @@ export function createTextBuffer(value: string | model.ITextBufferFactory, defau let MODEL_ID = 0; -/** - * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. - */ -function singleLetter(result: number): string { - const LETTERS_CNT = (CharCode.Z - CharCode.A + 1); - - result = result % (2 * LETTERS_CNT); - - if (result < LETTERS_CNT) { - return String.fromCharCode(CharCode.a + result); - } - - return String.fromCharCode(CharCode.A + result - LETTERS_CNT); -} - const LIMIT_FIND_COUNT = 999; export const LONG_LINE_BOUNDARY = 10000; @@ -343,7 +328,7 @@ export class TextModel extends Disposable implements model.ITextModel { } }); - this._instanceId = singleLetter(MODEL_ID); + this._instanceId = strings.singleLetterHash(MODEL_ID); this._lastDecorationId = 0; this._decorations = Object.create(null); this._decorationsTree = new DecorationsTrees(); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 7480fccf86bf7..cc2adefb59e04 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -367,6 +367,10 @@ export let completionKindFromString: { }; })(); +export const enum CompletionItemTag { + Deprecated = 1 +} + export const enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to @@ -396,6 +400,11 @@ export interface CompletionItem { * an icon is chosen by the editor. */ kind: CompletionItemKind; + /** + * A modifier to the `kind` which affect how the item + * is rendered, e.g. Deprecated is rendered with a strikeout + */ + tags?: ReadonlyArray; /** * A human-readable string with additional information * about this item, like type or symbol information. @@ -464,7 +473,7 @@ export interface CompletionItem { /** * @internal */ - [key: string]: any; + _id?: [number, number]; } export interface CompletionList { @@ -858,6 +867,9 @@ export const enum SymbolKind { TypeParameter = 25 } +export const enum SymbolTag { + Deprecated = 1, +} /** * @internal @@ -901,6 +913,7 @@ export interface DocumentSymbol { name: string; detail: string; kind: SymbolKind; + tags: ReadonlyArray; containerName?: string; range: IRange; selectionRange: IRange; diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index 4bca048eec17c..ed8d2fdb4ae37 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -78,7 +78,9 @@ export interface LanguageConfiguration { * * @deprecated Will be replaced by a better API soon. */ - __electricCharacterSupport?: IBracketElectricCharacterContribution; + __electricCharacterSupport?: { + docComment?: IDocComment; + }; } /** @@ -155,10 +157,6 @@ export interface OnEnterRule { action: EnterAction; } -export interface IBracketElectricCharacterContribution { - docComment?: IDocComment; -} - /** * Definition of documentation comments (e.g. Javadoc/JSdoc) */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index baf47379af988..cee0469e30e0e 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; -import { EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { EnterAction, FoldingRules, IAutoClosingPair, IndentAction, IndentationRule, LanguageConfiguration, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; import { createScopedLineTokens } from 'vs/editor/common/modes/supports'; import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair'; import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter'; @@ -97,16 +97,7 @@ export class RichEditSupport { public get electricCharacter(): BracketElectricCharacterSupport | null { if (!this._electricCharacter) { - let autoClosingPairs: IAutoClosingPairConditional[] = []; - if (this._conf.autoClosingPairs) { - autoClosingPairs = this._conf.autoClosingPairs; - } else if (this._conf.brackets) { - autoClosingPairs = this._conf.brackets.map(b => { - return { open: b[0], close: b[1] }; - }); - } - - this._electricCharacter = new BracketElectricCharacterSupport(this.brackets, autoClosingPairs, this._conf.__electricCharacterSupport); + this._electricCharacter = new BracketElectricCharacterSupport(this.brackets); } return this._electricCharacter; } @@ -261,7 +252,7 @@ export class LanguageConfigurationRegistryImpl { return value.characterPair || null; } - public getAutoClosingPairs(languageId: LanguageId): IAutoClosingPair[] { + public getAutoClosingPairs(languageId: LanguageId): StandardAutoClosingPairConditional[] { let characterPairSupport = this._getCharacterPairSupport(languageId); if (!characterPairSupport) { return []; @@ -285,13 +276,9 @@ export class LanguageConfigurationRegistryImpl { return characterPairSupport.getSurroundingPairs(); } - public shouldAutoClosePair(character: string, context: LineTokens, column: number): boolean { - let scopedLineTokens = createScopedLineTokens(context, column - 1); - let characterPairSupport = this._getCharacterPairSupport(scopedLineTokens.languageId); - if (!characterPairSupport) { - return false; - } - return characterPairSupport.shouldAutoClosePair(character, scopedLineTokens, column - scopedLineTokens.firstCharOffset); + public shouldAutoClosePair(autoClosingPair: StandardAutoClosingPairConditional, context: LineTokens, column: number): boolean { + const scopedLineTokens = createScopedLineTokens(context, column - 1); + return CharacterPairSupport.shouldAutoClosePair(autoClosingPair, scopedLineTokens, column - scopedLineTokens.firstCharOffset); } // end characterPair diff --git a/src/vs/editor/common/modes/supports/characterPair.ts b/src/vs/editor/common/modes/supports/characterPair.ts index bb8ecd4980ebd..b7cfa2ecdd70d 100644 --- a/src/vs/editor/common/modes/supports/characterPair.ts +++ b/src/vs/editor/common/modes/supports/characterPair.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CharacterPair, IAutoClosingPair, IAutoClosingPairConditional, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; +import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; import { ScopedLineTokens } from 'vs/editor/common/modes/supports'; export class CharacterPairSupport { @@ -15,7 +15,7 @@ export class CharacterPairSupport { private readonly _surroundingPairs: IAutoClosingPair[]; private readonly _autoCloseBefore: string; - constructor(config: { brackets?: CharacterPair[]; autoClosingPairs?: IAutoClosingPairConditional[], surroundingPairs?: IAutoClosingPair[], autoCloseBefore?: string }) { + constructor(config: LanguageConfiguration) { if (config.autoClosingPairs) { this._autoClosingPairs = config.autoClosingPairs.map(el => new StandardAutoClosingPairConditional(el)); } else if (config.brackets) { @@ -24,12 +24,18 @@ export class CharacterPairSupport { this._autoClosingPairs = []; } + if (config.__electricCharacterSupport && config.__electricCharacterSupport.docComment) { + const docComment = config.__electricCharacterSupport.docComment; + // IDocComment is legacy, only partially supported + this._autoClosingPairs.push(new StandardAutoClosingPairConditional({ open: docComment.open, close: docComment.close || '' })); + } + this._autoCloseBefore = typeof config.autoCloseBefore === 'string' ? config.autoCloseBefore : CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED; this._surroundingPairs = config.surroundingPairs || this._autoClosingPairs; } - public getAutoClosingPairs(): IAutoClosingPair[] { + public getAutoClosingPairs(): StandardAutoClosingPairConditional[] { return this._autoClosingPairs; } @@ -37,22 +43,15 @@ export class CharacterPairSupport { return this._autoCloseBefore; } - public shouldAutoClosePair(character: string, context: ScopedLineTokens, column: number): boolean { + public static shouldAutoClosePair(autoClosingPair: StandardAutoClosingPairConditional, context: ScopedLineTokens, column: number): boolean { // Always complete on empty line if (context.getTokenCount() === 0) { return true; } - let tokenIndex = context.findTokenIndexAtOffset(column - 2); - let standardTokenType = context.getStandardTokenType(tokenIndex); - - for (const autoClosingPair of this._autoClosingPairs) { - if (autoClosingPair.open === character) { - return autoClosingPair.isOK(standardTokenType); - } - } - - return false; + const tokenIndex = context.findTokenIndexAtOffset(column - 2); + const standardTokenType = context.getStandardTokenType(tokenIndex); + return autoClosingPair.isOK(standardTokenType); } public getSurroundingPairs(): IAutoClosingPair[] { diff --git a/src/vs/editor/common/modes/supports/electricCharacter.ts b/src/vs/editor/common/modes/supports/electricCharacter.ts index 878ef492e12d7..387994c28a42a 100644 --- a/src/vs/editor/common/modes/supports/electricCharacter.ts +++ b/src/vs/editor/common/modes/supports/electricCharacter.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAutoClosingPairConditional, IBracketElectricCharacterContribution, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; import { ScopedLineTokens, ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; @@ -12,29 +11,17 @@ import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports * @internal */ export interface IElectricAction { - // Only one of the following properties should be defined: - // The line will be indented at the same level of the line // which contains the matching given bracket type. - matchOpenBracket?: string; - - // The text will be appended after the electric character. - appendText?: string; + matchOpenBracket: string; } export class BracketElectricCharacterSupport { private readonly _richEditBrackets: RichEditBrackets | null; - private readonly _complexAutoClosePairs: StandardAutoClosingPairConditional[]; - constructor(richEditBrackets: RichEditBrackets | null, autoClosePairs: IAutoClosingPairConditional[], contribution: IBracketElectricCharacterContribution | null | undefined) { - contribution = contribution || {}; + constructor(richEditBrackets: RichEditBrackets | null) { this._richEditBrackets = richEditBrackets; - this._complexAutoClosePairs = autoClosePairs.filter(pair => pair.open.length > 1 && !!pair.close).map(el => new StandardAutoClosingPairConditional(el)); - if (contribution.docComment) { - // IDocComment is legacy, only partially supported - this._complexAutoClosePairs.push(new StandardAutoClosingPairConditional({ open: contribution.docComment.open, close: contribution.docComment.close || '' })); - } } public getElectricCharacters(): string[] { @@ -48,11 +35,6 @@ export class BracketElectricCharacterSupport { } } - // auto close - for (let pair of this._complexAutoClosePairs) { - result.push(pair.open.charAt(pair.open.length - 1)); - } - // Filter duplicate entries result = result.filter((item, pos, array) => { return array.indexOf(item) === pos; @@ -62,12 +44,6 @@ export class BracketElectricCharacterSupport { } public onElectricCharacter(character: string, context: ScopedLineTokens, column: number): IElectricAction | null { - return (this._onElectricAutoClose(character, context, column) || - this._onElectricAutoIndent(character, context, column)); - } - - private _onElectricAutoIndent(character: string, context: ScopedLineTokens, column: number): IElectricAction | null { - if (!this._richEditBrackets || this._richEditBrackets.brackets.length === 0) { return null; } @@ -103,44 +79,4 @@ export class BracketElectricCharacterSupport { matchOpenBracket: bracketText }; } - - private _onElectricAutoClose(character: string, context: ScopedLineTokens, column: number): IElectricAction | null { - if (!this._complexAutoClosePairs.length) { - return null; - } - - let line = context.getLineContent(); - - for (let i = 0, len = this._complexAutoClosePairs.length; i < len; i++) { - let pair = this._complexAutoClosePairs[i]; - - // See if the right electric character was pressed - if (character !== pair.open.charAt(pair.open.length - 1)) { - continue; - } - - // check if the full open bracket matches - let start = column - pair.open.length + 1; - let actual = line.substring(start - 1, column - 1) + character; - if (actual !== pair.open) { - continue; - } - - let lastTokenIndex = context.findTokenIndexAtOffset(column - 1); - let lastTokenStandardType = context.getStandardTokenType(lastTokenIndex); - // If we're in a scope listed in 'notIn', do nothing - if (!pair.isOK(lastTokenStandardType)) { - continue; - } - - // If this line already contains the closing tag, do nothing. - if (line.indexOf(pair.close, column - 1) >= 0) { - continue; - } - - return { appendText: pair.close }; - } - - return null; - } } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 46d295d2398c3..a705c132478b7 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -581,6 +581,10 @@ export enum CompletionItemKind { Snippet = 25 } +export enum CompletionItemTag { + Deprecated = 1 +} + export enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to @@ -656,4 +660,8 @@ export enum SymbolKind { Event = 23, Operator = 24, TypeParameter = 25 +} + +export enum SymbolTag { + Deprecated = 1 } \ No newline at end of file diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index 2967fb3b003f3..d6673ac8b8fbe 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -63,14 +63,14 @@ export class LinesLayout { * @param heightInPx The height of the whitespace, in pixels. * @return An id that can be used later to mutate or delete the whitespace */ - public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): number { + public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { return this._whitespaces.insertWhitespace(afterLineNumber, ordinal, heightInPx, minWidth); } /** * Change properties associated with a certain whitespace. */ - public changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean { + public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { return this._whitespaces.changeWhitespace(id, newAfterLineNumber, newHeight); } @@ -80,7 +80,7 @@ export class LinesLayout { * @param id The whitespace to remove * @return Returns true if the whitespace is found and it is removed. */ - public removeWhitespace(id: number): boolean { + public removeWhitespace(id: string): boolean { return this._whitespaces.removeWhitespace(id); } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index f753f65d393b3..ad24479b4b2a9 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -173,13 +173,13 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- IVerticalLayoutProvider - public addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): number { + public addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): string { return this._linesLayout.insertWhitespace(afterLineNumber, ordinal, height, minWidth); } - public changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean { + public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { return this._linesLayout.changeWhitespace(id, newAfterLineNumber, newHeight); } - public removeWhitespace(id: number): boolean { + public removeWhitespace(id: string): boolean { return this._linesLayout.removeWhitespace(id); } public getVerticalOffsetForLineNumber(lineNumber: number): number { diff --git a/src/vs/editor/common/viewLayout/whitespaceComputer.ts b/src/vs/editor/common/viewLayout/whitespaceComputer.ts index 537c36ac80740..8e5dd347e8306 100644 --- a/src/vs/editor/common/viewLayout/whitespaceComputer.ts +++ b/src/vs/editor/common/viewLayout/whitespaceComputer.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as strings from 'vs/base/common/strings'; + export interface IEditorWhitespace { - readonly id: number; + readonly id: string; readonly afterLineNumber: number; readonly heightInLines: number; } @@ -15,6 +17,10 @@ export interface IEditorWhitespace { */ export class WhitespaceComputer { + private static INSTANCE_COUNT = 0; + + private readonly _instanceId: string; + /** * heights[i] is the height in pixels for whitespace at index i */ @@ -48,7 +54,7 @@ export class WhitespaceComputer { /** * ids[i] is the whitespace id of whitespace at index i */ - private readonly _ids: number[]; + private readonly _ids: string[]; /** * index at which a whitespace is positioned (inside heights, afterLineNumbers, prefixSum members) @@ -65,6 +71,7 @@ export class WhitespaceComputer { private _minWidth: number; constructor() { + this._instanceId = strings.singleLetterHash(++WhitespaceComputer.INSTANCE_COUNT); this._heights = []; this._minWidths = []; this._ids = []; @@ -113,21 +120,20 @@ export class WhitespaceComputer { * @param heightInPx The height of the whitespace, in pixels. * @return An id that can be used later to mutate or delete the whitespace */ - public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): number { + public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { afterLineNumber = afterLineNumber | 0; ordinal = ordinal | 0; heightInPx = heightInPx | 0; minWidth = minWidth | 0; - let id = (++this._lastWhitespaceId); + let id = this._instanceId + (++this._lastWhitespaceId); let insertionIndex = WhitespaceComputer.findInsertionIndex(this._afterLineNumbers, afterLineNumber, this._ordinals, ordinal); this._insertWhitespaceAtIndex(id, insertionIndex, afterLineNumber, ordinal, heightInPx, minWidth); this._minWidth = -1; /* marker for not being computed */ return id; } - private _insertWhitespaceAtIndex(id: number, insertIndex: number, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): void { - id = id | 0; + private _insertWhitespaceAtIndex(id: string, insertIndex: number, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): void { insertIndex = insertIndex | 0; afterLineNumber = afterLineNumber | 0; ordinal = ordinal | 0; @@ -150,15 +156,14 @@ export class WhitespaceComputer { } } - this._whitespaceId2Index[id.toString()] = insertIndex; + this._whitespaceId2Index[id] = insertIndex; this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, insertIndex - 1); } /** * Change properties associated with a certain whitespace. */ - public changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean { - id = id | 0; + public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { newAfterLineNumber = newAfterLineNumber | 0; newHeight = newHeight | 0; @@ -175,13 +180,11 @@ export class WhitespaceComputer { * @param newHeightInPx The new height of the whitespace, in pixels * @return Returns true if the whitespace is found and if the new height is different than the old height */ - public changeWhitespaceHeight(id: number, newHeightInPx: number): boolean { - id = id | 0; + public changeWhitespaceHeight(id: string, newHeightInPx: number): boolean { newHeightInPx = newHeightInPx | 0; - let sid = id.toString(); - if (this._whitespaceId2Index.hasOwnProperty(sid)) { - let index = this._whitespaceId2Index[sid]; + if (this._whitespaceId2Index.hasOwnProperty(id)) { + let index = this._whitespaceId2Index[id]; if (this._heights[index] !== newHeightInPx) { this._heights[index] = newHeightInPx; this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, index - 1); @@ -198,13 +201,11 @@ export class WhitespaceComputer { * @param newAfterLineNumber The new line number the whitespace will follow * @return Returns true if the whitespace is found and if the new line number is different than the old line number */ - public changeWhitespaceAfterLineNumber(id: number, newAfterLineNumber: number): boolean { - id = id | 0; + public changeWhitespaceAfterLineNumber(id: string, newAfterLineNumber: number): boolean { newAfterLineNumber = newAfterLineNumber | 0; - let sid = id.toString(); - if (this._whitespaceId2Index.hasOwnProperty(sid)) { - let index = this._whitespaceId2Index[sid]; + if (this._whitespaceId2Index.hasOwnProperty(id)) { + let index = this._whitespaceId2Index[id]; if (this._afterLineNumbers[index] !== newAfterLineNumber) { // `afterLineNumber` changed for this whitespace @@ -236,14 +237,10 @@ export class WhitespaceComputer { * @param id The whitespace to remove * @return Returns true if the whitespace is found and it is removed. */ - public removeWhitespace(id: number): boolean { - id = id | 0; - - let sid = id.toString(); - - if (this._whitespaceId2Index.hasOwnProperty(sid)) { - let index = this._whitespaceId2Index[sid]; - delete this._whitespaceId2Index[sid]; + public removeWhitespace(id: string): boolean { + if (this._whitespaceId2Index.hasOwnProperty(id)) { + let index = this._whitespaceId2Index[id]; + delete this._whitespaceId2Index[id]; this._removeWhitespaceAtIndex(index); this._minWidth = -1; /* marker for not being computed */ return true; @@ -459,7 +456,7 @@ export class WhitespaceComputer { * @param index The index of the whitespace. * @return `id` of whitespace at `index`. */ - public getIdForWhitespaceIndex(index: number): number { + public getIdForWhitespaceIndex(index: number): string { index = index | 0; return this._ids[index]; diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index fe832ef3707df..0f16d0a84a299 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -17,7 +17,7 @@ import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceCompute import { ITheme } from 'vs/platform/theme/common/themeService'; export interface IViewWhitespaceViewportData { - readonly id: number; + readonly id: string; readonly afterLineNumber: number; readonly verticalOffset: number; readonly height: number; @@ -74,15 +74,15 @@ export interface IViewLayout { * Reserve rendering space. * @return an identifier that can be later used to remove or change the whitespace. */ - addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): number; + addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): string; /** * Change the properties of a whitespace. */ - changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean; + changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean; /** * Remove rendering space */ - removeWhitespace(id: number): boolean; + removeWhitespace(id: string): boolean; /** * Get the layout information for whitespaces currently in the viewport */ diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 93873f17ffbcd..f706070a89bb7 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -117,8 +117,8 @@ function getCodeActionProviders( } registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise> { - const { resource, range, kind } = args; - if (!(resource instanceof URI) || !Range.isIRange(range)) { + const { resource, rangeOrSelection, kind } = args; + if (!(resource instanceof URI)) { throw illegalArgument(); } @@ -127,9 +127,19 @@ registerLanguageCommand('_executeCodeActionProvider', async function (accessor, throw illegalArgument(); } + const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection) + ? Selection.liftSelection(rangeOrSelection) + : Range.isIRange(rangeOrSelection) + ? model.validateRange(rangeOrSelection) + : undefined; + + if (!validatedRangeOrSelection) { + throw illegalArgument(); + } + const codeActionSet = await getCodeActions( model, - model.validateRange(range), + validatedRangeOrSelection, { type: 'manual', filter: { includeSourceActions: true, kind: kind && kind.value ? new CodeActionKind(kind.value) : undefined } }, CancellationToken.None); diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 1aed8453777cd..726133c3f7f6e 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -32,7 +32,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { private _currentCodeLensModel: CodeLensModel | undefined; private _modelChangeCounter: number = 0; private _currentResolveCodeLensSymbolsPromise: CancelablePromise | undefined; - private _detectVisibleLenses!: RunOnceScheduler; + private _detectVisibleLenses: RunOnceScheduler | undefined; constructor( private readonly _editor: editorBrowser.ICodeEditor, @@ -121,9 +121,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { } } - this._detectVisibleLenses = new RunOnceScheduler(() => { - this._onViewportChanged(); - }, 250); + const detectVisibleLenses = this._detectVisibleLenses = new RunOnceScheduler(() => this._onViewportChanged(), 250); const scheduler = new RunOnceScheduler(() => { const counterValue = ++this._modelChangeCounter; @@ -145,12 +143,12 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { // render lenses this._renderCodeLensSymbols(result); - this._detectVisibleLenses.schedule(); + detectVisibleLenses.schedule(); } }, onUnexpectedError); }, 250); this._localToDispose.add(scheduler); - this._localToDispose.add(this._detectVisibleLenses); + this._localToDispose.add(detectVisibleLenses); this._localToDispose.add(this._editor.onDidChangeModelContent(() => { this._editor.changeDecorations(decorationsAccessor => { this._editor.changeViewZones(viewZonesAccessor => { @@ -179,17 +177,17 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { }); // Compute new `visible` code lenses - this._detectVisibleLenses.schedule(); + detectVisibleLenses.schedule(); // Ask for all references again scheduler.schedule(); })); this._localToDispose.add(this._editor.onDidScrollChange(e => { if (e.scrollTopChanged && this._lenses.length > 0) { - this._detectVisibleLenses.schedule(); + detectVisibleLenses.schedule(); } })); this._localToDispose.add(this._editor.onDidLayoutChange(() => { - this._detectVisibleLenses.schedule(); + detectVisibleLenses.schedule(); })); this._localToDispose.add(toDisposable(() => { if (this._editor.getModel()) { @@ -281,7 +279,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { groupsIndex++; codeLensIndex++; } else { - this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, helper, viewZoneAccessor, () => this._detectVisibleLenses.schedule())); + this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); codeLensIndex++; groupsIndex++; } @@ -295,7 +293,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { // Create extra symbols while (groupsIndex < groups.length) { - this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, helper, viewZoneAccessor, () => this._detectVisibleLenses.schedule())); + this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); groupsIndex++; } diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index cb41dc9f45930..5ae20b27edb7a 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -193,7 +193,7 @@ export class CodeLensWidget { private readonly _editor: editorBrowser.ICodeEditor; private readonly _viewZone!: CodeLensViewZone; - private readonly _viewZoneId!: number; + private readonly _viewZoneId!: string; private readonly _contentWidget!: CodeLensContentWidget; private _decorationIds: string[]; private _data: CodeLensItem[]; diff --git a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css index 4e312f7dfe6fe..0504a233a6388 100644 --- a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css +++ b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css @@ -3,6 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench .monaco-icon-label.deprecated { + text-decoration: line-through; + opacity: 0.66; +} + .monaco-workbench .symbol-icon.inline { background-position: left center; padding-left: 20px; diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index 180b0d5a55c0a..e5004629993f1 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -202,16 +202,33 @@ export class OutlineGroup extends TreeElement { } } +class MovingAverage { + + private _n = 1; + private _val = 0; + + update(value: number): this { + this._val = this._val + (value - this._val) / this._n; + this._n += 1; + return this; + } + + get value(): number { + return this._val; + } +} + export class OutlineModel extends TreeElement { + private static readonly _requestDurations = new LRUCache(50, 0.7); private static readonly _requests = new LRUCache, model: OutlineModel | undefined }>(9, 0.75); private static readonly _keys = new class { private _counter = 1; private _data = new WeakMap(); - for(textModel: ITextModel): string { - return `${textModel.id}/${textModel.getVersionId()}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`; + for(textModel: ITextModel, version: boolean): string { + return `${textModel.id}/${version ? textModel.getVersionId() : ''}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`; } private _hash(providers: DocumentSymbolProvider[]): string { @@ -231,7 +248,7 @@ export class OutlineModel extends TreeElement { static create(textModel: ITextModel, token: CancellationToken): Promise { - let key = this._keys.for(textModel); + let key = this._keys.for(textModel, true); let data = OutlineModel._requests.get(key); if (!data) { @@ -243,6 +260,18 @@ export class OutlineModel extends TreeElement { model: undefined, }; OutlineModel._requests.set(key, data); + + // keep moving average of request durations + const now = Date.now(); + data.promise.then(() => { + let key = this._keys.for(textModel, false); + let avg = this._requestDurations.get(key); + if (!avg) { + avg = new MovingAverage(); + this._requestDurations.set(key, avg); + } + avg.update(Date.now() - now); + }); } if (data!.model) { @@ -272,7 +301,18 @@ export class OutlineModel extends TreeElement { }); } - static _create(textModel: ITextModel, token: CancellationToken): Promise { + static getRequestDelay(textModel: ITextModel | null): number { + if (!textModel) { + return 350; + } + const avg = this._requestDurations.get(this._keys.for(textModel, false)); + if (!avg) { + return 350; + } + return Math.max(350, Math.floor(1.3 * avg.value)); + } + + private static _create(textModel: ITextModel, token: CancellationToken): Promise { const cts = new CancellationTokenSource(token); const result = new OutlineModel(textModel); diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 365b3e45c1399..84ac8bb704c61 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -12,7 +12,7 @@ import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import 'vs/css!./media/outlineTree'; import 'vs/css!./media/symbol-icons'; import { Range } from 'vs/editor/common/core/range'; -import { SymbolKind, symbolKindToCssClass } from 'vs/editor/common/modes'; +import { SymbolKind, symbolKindToCssClass, SymbolTag } from 'vs/editor/common/modes'; import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; @@ -127,6 +127,10 @@ export class OutlineElementRenderer implements ITreeRenderer= 0) { + options.extraClasses.push(`deprecated`); + options.matches = []; + } template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options); this._renderMarkerInfo(element, template); } diff --git a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts index b15a14510720c..b41a7f8dd8f27 100644 --- a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts +++ b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts @@ -76,6 +76,7 @@ suite('OutlineModel', function () { name, detail: 'fake', kind: SymbolKind.Boolean, + tags: [], selectionRange: range, range: range }; diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index ed3544d10ff8a..664bc8c8bf32e 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -12,19 +12,20 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; +import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel'; import { FindOptionsWidget } from 'vs/editor/contrib/find/findOptionsWidget'; import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/findState'; import { FindWidget, IFindController } from 'vs/editor/contrib/find/findWidget'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -75,7 +76,7 @@ export class CommonFindController extends Disposable implements editorCommon.IEd protected _state: FindReplaceState; protected _updateHistoryDelayer: Delayer; private _model: FindModelBoundToEditorModel | null; - private readonly _storageService: IStorageService; + protected readonly _storageService: IStorageService; private readonly _clipboardService: IClipboardService; protected readonly _contextKeyService: IContextKeyService; @@ -383,10 +384,11 @@ export class FindController extends CommonFindController implements IFindControl @IContextKeyService _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, - @IStorageService storageService: IStorageService, - @optional(IClipboardService) clipboardService: IClipboardService + @INotificationService private readonly _notificationService: INotificationService, + @IStorageService _storageService: IStorageService, + @optional(IClipboardService) clipboardService: IClipboardService, ) { - super(editor, _contextKeyService, storageService, clipboardService); + super(editor, _contextKeyService, _storageService, clipboardService); this._widget = null; this._findOptionsWidget = null; } @@ -422,7 +424,7 @@ export class FindController extends CommonFindController implements IFindControl } private _createFindWidget() { - this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService)); + this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService, this._themeService)); } } @@ -540,6 +542,27 @@ export class NextMatchFindAction extends MatchFindAction { } } +export class NextMatchFindAction2 extends MatchFindAction { + + constructor() { + super({ + id: FIND_IDS.NextMatchFindAction, + label: nls.localize('findNextMatchAction', "Find Next"), + alias: 'Find Next', + precondition: undefined, + kbOpts: { + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + }); + } + + protected _run(controller: CommonFindController): boolean { + return controller.moveToNextMatch(); + } +} + export class PreviousMatchFindAction extends MatchFindAction { constructor() { @@ -562,6 +585,27 @@ export class PreviousMatchFindAction extends MatchFindAction { } } +export class PreviousMatchFindAction2 extends MatchFindAction { + + constructor() { + super({ + id: FIND_IDS.PreviousMatchFindAction, + label: nls.localize('findPreviousMatchAction', "Find Previous"), + alias: 'Find Previous', + precondition: undefined, + kbOpts: { + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), + primary: KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + }); + } + + protected _run(controller: CommonFindController): boolean { + return controller.moveToPrevMatch(); + } +} + export abstract class SelectionMatchFindAction extends EditorAction { public run(accessor: ServicesAccessor | null, editor: ICodeEditor): void { let controller = CommonFindController.get(editor); @@ -695,7 +739,9 @@ registerEditorContribution(FindController); registerEditorAction(StartFindAction); registerEditorAction(StartFindWithSelectionAction); registerEditorAction(NextMatchFindAction); +registerEditorAction(NextMatchFindAction2); registerEditorAction(PreviousMatchFindAction); +registerEditorAction(PreviousMatchFindAction2); registerEditorAction(NextSelectionMatchFindAction); registerEditorAction(PreviousSelectionMatchFindAction); registerEditorAction(StartFindReplaceAction); @@ -781,6 +827,17 @@ registerEditorCommand(new FindCommand({ } })); +registerEditorCommand(new FindCommand({ + id: FIND_IDS.ReplaceOneAction, + precondition: CONTEXT_FIND_WIDGET_VISIBLE, + handler: x => x.replace(), + kbOpts: { + weight: KeybindingWeight.EditorContrib + 5, + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED), + primary: KeyCode.Enter + } +})); + registerEditorCommand(new FindCommand({ id: FIND_IDS.ReplaceAllAction, precondition: CONTEXT_FIND_WIDGET_VISIBLE, @@ -792,6 +849,20 @@ registerEditorCommand(new FindCommand({ } })); +registerEditorCommand(new FindCommand({ + id: FIND_IDS.ReplaceAllAction, + precondition: CONTEXT_FIND_WIDGET_VISIBLE, + handler: x => x.replaceAll(), + kbOpts: { + weight: KeybindingWeight.EditorContrib + 5, + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED), + primary: undefined, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.Enter, + } + } +})); + registerEditorCommand(new FindCommand({ id: FIND_IDS.SelectAllMatchesAction, precondition: CONTEXT_FIND_WIDGET_VISIBLE, diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index 7e1140442eac4..693157f51f60a 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -32,8 +32,8 @@ .monaco-editor .find-widget { position: absolute; z-index: 10; - top: -44px; /* find input height + shadow (10px) */ - height: 34px; /* find input height */ + top: -44px; + height: 33px; overflow: hidden; line-height: 19px; transition: top 200ms linear; @@ -47,12 +47,10 @@ /* Find widget when replace is toggled on */ .monaco-editor .find-widget.replaceToggled { top: -74px; /* find input height + replace input height + shadow (10px) */ - height: 64px; /* find input height + replace input height */ } .monaco-editor .find-widget.replaceToggled > .replace-part { display: flex; display: -webkit-flex; - align-items: center; } .monaco-editor .find-widget.visible, @@ -60,13 +58,31 @@ top: 0; } +/* Multiple line find widget */ + +.monaco-editor .find-widget.multipleline { + top: unset; + bottom: 10px; +} + +.monaco-editor .find-widget.multipleline.visible, +.monaco-editor .find-widget.multipleline.replaceToggled.visible { + top: 0px; + bottom: unset; +} + +.monaco-editor .find-widget .monaco-inputbox.synthetic-focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; +} + .monaco-editor .find-widget .monaco-inputbox .input { background-color: transparent; /* Style to compensate for //winjs */ min-height: 0; } -.monaco-editor .find-widget .replace-input .input { +.monaco-editor .find-widget .monaco-findInput .input { font-size: 13px; } @@ -76,29 +92,38 @@ font-size: 12px; display: flex; display: -webkit-flex; - align-items: center; } .monaco-editor .find-widget > .find-part .monaco-inputbox, .monaco-editor .find-widget > .replace-part .monaco-inputbox { - height: 25px; + min-height: 25px; } -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input { - width: 100% !important; - padding-right: 66px; -} -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input { +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { padding-right: 22px; } .monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input { +.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .mirror, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { padding-top: 2px; padding-bottom: 2px; } +.monaco-editor .find-widget > .find-part .find-actions { + height: 25px; + display: flex; + align-items: center; +} + +.monaco-editor .find-widget > .replace-part .replace-actions { + height: 25px; + display: flex; + align-items: center; +} + .monaco-editor .find-widget .monaco-findInput { vertical-align: middle; display: flex; @@ -106,6 +131,16 @@ flex:1; } +.monaco-editor .find-widget .monaco-findInput .monaco-scrollable-element { + /* Make sure textarea inherits the width correctly */ + width: 100%; +} + +.monaco-editor .find-widget .monaco-findInput .monaco-scrollable-element .scrollbar.vertical { + /* Hide vertical scrollbar */ + opacity: 0; +} + .monaco-editor .find-widget .matchesCount { display: flex; display: -webkit-flex; @@ -237,15 +272,17 @@ display: none; } -.monaco-editor .find-widget > .replace-part > .replace-input { +.monaco-editor .find-widget > .replace-part > .monaco-findInput { position: relative; display: flex; display: -webkit-flex; vertical-align: middle; - width: auto !important; + flex: auto; + flex-grow: 0; + flex-shrink: 0; } -.monaco-editor .find-widget > .replace-part > .replace-input > .controls { +.monaco-editor .find-widget > .replace-part > .monaco-findInput > .controls { position: absolute; top: 3px; right: 2px; diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 529d582ee47a7..2bfbe8388d3e1 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -9,11 +9,12 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; -import { HistoryInputBox, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IHorizontalSashLayoutProvider, ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { Widget } from 'vs/base/browser/ui/widget'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { Delayer } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -28,11 +29,12 @@ import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_REPLACE_INPUT_FOCUSED, FIND_IDS, MA import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetResizeBorder, errorForeground, inputActiveOptionBorder, inputActiveOptionBackground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetResizeBorder, errorForeground, inputActiveOptionBorder, inputActiveOptionBackground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export interface IFindController { replace(): void; @@ -48,7 +50,6 @@ const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind' const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace"); -const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case"); const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); @@ -59,14 +60,12 @@ const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results"); const FIND_WIDGET_INITIAL_WIDTH = 411; const PART_WIDTH = 275; const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54; -const REPLACE_INPUT_AREA_WIDTH = FIND_INPUT_AREA_WIDTH; let MAX_MATCHES_COUNT_WIDTH = 69; let FIND_ALL_CONTROLS_WIDTH = 17/** Find Input margin-left */ + (MAX_MATCHES_COUNT_WIDTH + 3 + 1) /** Match Results */ + 23 /** Button */ * 4 + 2/** sash */; -const FIND_INPUT_AREA_HEIGHT = 34; // The height of Find Widget when Replace Input is not visible. -const FIND_REPLACE_AREA_HEIGHT = 64; // The height of Find Widget when Replace Input is visible. - +const FIND_INPUT_AREA_HEIGHT = 33; // The height of Find Widget when Replace Input is not visible. +const ctrlEnterReplaceAllWarningPromptedKey = 'ctrlEnterReplaceAll.windows.donotask'; export class FindWidgetViewZone implements IViewZone { public readonly afterLineNumber: number; @@ -84,6 +83,22 @@ export class FindWidgetViewZone implements IViewZone { } } +function stopPropagationForMultiLineUpwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) { + const isMultiline = !!value.match(/\n/); + if (textarea && isMultiline && textarea.selectionStart > 0) { + event.stopPropagation(); + return; + } +} + +function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) { + const isMultiline = !!value.match(/\n/); + if (textarea && isMultiline && textarea.selectionEnd < textarea.value.length) { + event.stopPropagation(); + return; + } +} + export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSashLayoutProvider { private static readonly ID = 'editor.contrib.findWidget'; private readonly _codeEditor: ICodeEditor; @@ -92,10 +107,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private readonly _contextViewProvider: IContextViewProvider; private readonly _keybindingService: IKeybindingService; private readonly _contextKeyService: IContextKeyService; + private readonly _storageService: IStorageService; + private readonly _notificationService: INotificationService; private _domNode!: HTMLElement; + private _cachedHeight: number | null; private _findInput!: FindInput; - private _replaceInputBox!: HistoryInputBox; + private _replaceInput!: ReplaceInput; private _toggleReplaceBtn!: SimpleButton; private _matchesCount!: HTMLElement; @@ -103,20 +121,20 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _nextBtn!: SimpleButton; private _toggleSelectionFind!: SimpleCheckbox; private _closeBtn!: SimpleButton; - private _preserveCase!: Checkbox; private _replaceBtn!: SimpleButton; private _replaceAllBtn!: SimpleButton; private _isVisible: boolean; private _isReplaceVisible: boolean; private _ignoreChangeEvent: boolean; + private _ctrlEnterReplaceAllWarningPrompted: boolean; private readonly _findFocusTracker: dom.IFocusTracker; private readonly _findInputFocused: IContextKey; private readonly _replaceFocusTracker: dom.IFocusTracker; private readonly _replaceInputFocused: IContextKey; private _viewZone?: FindWidgetViewZone; - private _viewZoneId?: number; + private _viewZoneId?: string; private _resizeSash!: Sash; private _resized!: boolean; @@ -129,7 +147,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas contextViewProvider: IContextViewProvider, keybindingService: IKeybindingService, contextKeyService: IContextKeyService, - themeService: IThemeService + themeService: IThemeService, + storageService: IStorageService, + notificationService: INotificationService, ) { super(); this._codeEditor = codeEditor; @@ -138,6 +158,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._contextViewProvider = contextViewProvider; this._keybindingService = keybindingService; this._contextKeyService = contextKeyService; + this._storageService = storageService; + this._notificationService = notificationService; + + this._ctrlEnterReplaceAllWarningPrompted = !!storageService.getBoolean(ctrlEnterReplaceAllWarningPromptedKey, StorageScope.GLOBAL); this._isVisible = false; this._isReplaceVisible = false; @@ -149,6 +173,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._buildDomNode(); this._updateButtons(); this._tryUpdateWidgetWidth(); + this._findInput.inputBox.layout(); this._register(this._codeEditor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => { if (e.readOnly) { @@ -203,7 +228,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas })); this._replaceInputFocused = CONTEXT_REPLACE_INPUT_FOCUSED.bindTo(contextKeyService); - this._replaceFocusTracker = this._register(dom.trackFocus(this._replaceInputBox.inputElement)); + this._replaceFocusTracker = this._register(dom.trackFocus(this._replaceInput.inputBox.inputElement)); this._register(this._replaceFocusTracker.onDidFocus(() => { this._replaceInputFocused.set(true); this._updateSearchScope(); @@ -224,15 +249,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (!this._isVisible) { return; } - if (this._viewZoneId === undefined) { - return; - } - this._codeEditor.changeViewZones((accessor) => { - if (this._viewZoneId) { - accessor.removeZone(this._viewZoneId); - } - this._viewZoneId = undefined; - }); + this._viewZoneId = undefined; })); @@ -272,6 +289,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _onStateChanged(e: FindReplaceStateChangedEvent): void { if (e.searchString) { + if (this._state.searchString.indexOf('\n') >= 0) { + dom.addClass(this._domNode, 'multipleline'); + } else { + dom.removeClass(this._domNode, 'multipleline'); + } + try { this._ignoreChangeEvent = true; this._findInput.setValue(this._state.searchString); @@ -281,7 +304,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._updateButtons(); } if (e.replaceString) { - this._replaceInputBox.value = this._state.replaceString; + this._replaceInput.inputBox.value = this._state.replaceString; } if (e.isRevealed) { if (this._state.isRevealed) { @@ -294,8 +317,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (this._state.isReplaceRevealed) { if (!this._codeEditor.getConfiguration().readOnly && !this._isReplaceVisible) { this._isReplaceVisible = true; - this._replaceInputBox.width = this._findInput.inputBox.width; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); this._updateButtons(); + this._replaceInput.inputBox.layout(); } } else { if (this._isReplaceVisible) { @@ -304,6 +328,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } } } + if ((e.isRevealed || e.isReplaceRevealed) && (this._state.isRevealed || this._state.isReplaceRevealed)) { + if (this._tryUpdateHeight()) { + this._showViewZone(); + } + } + if (e.isRegex) { this._findInput.setRegex(this._state.isRegex); } @@ -345,7 +375,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._findInput.inputBox.addToHistory(); } if (this._state.replaceString) { - this._replaceInputBox.addToHistory(); + this._replaceInput.inputBox.addToHistory(); } } @@ -410,7 +440,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _updateButtons(): void { this._findInput.setEnabled(this._isVisible); - this._replaceInputBox.setEnabled(this._isVisible && this._isReplaceVisible); + this._replaceInput.setEnabled(this._isVisible && this._isReplaceVisible); this._updateToggleSelectionFindButton(); this._closeBtn.setEnabled(this._isVisible); @@ -520,12 +550,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } this._codeEditor.changeViewZones((accessor) => { - if (this._state.isReplaceRevealed) { - viewZone.heightInPx = FIND_REPLACE_AREA_HEIGHT; - } else { - viewZone.heightInPx = FIND_INPUT_AREA_HEIGHT; - } - + viewZone.heightInPx = this._getHeight(); this._viewZoneId = accessor.addZone(viewZone); // scroll top adjust to make sure the editor doesn't scroll when adding viewzone at the beginning. this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + viewZone.heightInPx); @@ -533,30 +558,47 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } private _showViewZone(adjustScroll: boolean = true) { - const viewZone = this._viewZone; - if (!this._isVisible || !viewZone) { + if (!this._isVisible) { return; } - this._codeEditor.changeViewZones((accessor) => { - let scrollAdjustment = FIND_INPUT_AREA_HEIGHT; + const addExtraSpaceOnTop = this._codeEditor.getConfiguration().contribInfo.find.addExtraSpaceOnTop; + if (!addExtraSpaceOnTop) { + return; + } + + if (this._viewZone === undefined) { + this._viewZone = new FindWidgetViewZone(0); + } + + const viewZone = this._viewZone; + + this._codeEditor.changeViewZones((accessor) => { if (this._viewZoneId !== undefined) { - if (this._state.isReplaceRevealed) { - viewZone.heightInPx = FIND_REPLACE_AREA_HEIGHT; - scrollAdjustment = FIND_REPLACE_AREA_HEIGHT - FIND_INPUT_AREA_HEIGHT; - } else { - viewZone.heightInPx = FIND_INPUT_AREA_HEIGHT; - scrollAdjustment = FIND_INPUT_AREA_HEIGHT - FIND_REPLACE_AREA_HEIGHT; + // the view zone already exists, we need to update the height + const newHeight = this._getHeight(); + if (newHeight === viewZone.heightInPx) { + return; } - accessor.removeZone(this._viewZoneId); + + let scrollAdjustment = newHeight - viewZone.heightInPx; + viewZone.heightInPx = newHeight; + accessor.layoutZone(this._viewZoneId); + + if (adjustScroll) { + this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment); + } + + return; } else { - viewZone.heightInPx = FIND_INPUT_AREA_HEIGHT; - } - this._viewZoneId = accessor.addZone(viewZone); + const scrollAdjustment = this._getHeight(); + viewZone.heightInPx = scrollAdjustment; + this._viewZoneId = accessor.addZone(viewZone); - if (adjustScroll) { - this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment); + if (adjustScroll) { + this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment); + } } }); } @@ -592,8 +634,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), }; this._findInput.style(inputStyles); - this._replaceInputBox.style(inputStyles); - this._preserveCase.style(inputStyles); + this._replaceInput.style(inputStyles); } private _tryUpdateWidgetWidth() { @@ -623,7 +664,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (widgetWidth > FIND_WIDGET_INITIAL_WIDTH) { // as the widget is resized by users, we may need to change the max width of the widget as the editor width changes. this._domNode.style.maxWidth = `${editorWidth - 28 - minimapWidth - 15}px`; - this._replaceInputBox.inputElement.style.width = `${dom.getTotalWidth(this._findInput.inputBox.inputElement)}px`; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); return; } } @@ -647,13 +688,47 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } if (this._resized) { - let findInputWidth = dom.getTotalWidth(this._findInput.inputBox.inputElement); + this._findInput.inputBox.layout(); + let findInputWidth = this._findInput.inputBox.width; if (findInputWidth > 0) { - this._replaceInputBox.inputElement.style.width = `${findInputWidth}px`; + this._replaceInput.width = findInputWidth; } } } + private _getHeight(): number { + let totalheight = 0; + + // find input margin top + totalheight += 4; + + // find input height + totalheight += this._findInput.inputBox.height + 2 /** input box border */; + + if (this._isReplaceVisible) { + // replace input margin + totalheight += 4; + + totalheight += this._replaceInput.inputBox.height + 2 /** input box border */; + } + + // margin bottom + totalheight += 4; + return totalheight; + } + + private _tryUpdateHeight(): boolean { + const totalHeight = this._getHeight(); + if (this._cachedHeight !== null && this._cachedHeight === totalHeight) { + return false; + } + + this._cachedHeight = totalHeight; + this._domNode.style.height = `${totalHeight}px`; + + return true; + } + // ----- Public public focusFindInput(): void { @@ -663,9 +738,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } public focusReplaceInput(): void { - this._replaceInputBox.select(); + this._replaceInput.select(); // Edge browser requires focus() in addition to select() - this._replaceInputBox.focus(); + this._replaceInput.focus(); } public highlightFindOptions(): void { @@ -700,22 +775,25 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } private _onFindInputKeyDown(e: IKeyboardEvent): void { - - if (e.equals(KeyCode.Enter)) { - this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().then(undefined, onUnexpectedError); - e.preventDefault(); - return; - } - - if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().then(undefined, onUnexpectedError); - e.preventDefault(); - return; + if (e.equals(KeyMod.WinCtrl | KeyCode.Enter)) { + const inputElement = this._findInput.inputBox.inputElement; + const start = inputElement.selectionStart; + const end = inputElement.selectionEnd; + const content = inputElement.value; + + if (start && end) { + const value = content.substr(0, start) + '\n' + content.substr(end); + this._findInput.inputBox.value = value; + inputElement.setSelectionRange(start + 1, start + 1); + this._findInput.inputBox.layout(); + e.preventDefault(); + return; + } } if (e.equals(KeyCode.Tab)) { if (this._isReplaceVisible) { - this._replaceInputBox.focus(); + this._replaceInput.focus(); } else { this._findInput.focusOnCaseSensitive(); } @@ -728,20 +806,43 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas e.preventDefault(); return; } + + if (e.equals(KeyCode.UpArrow)) { + return stopPropagationForMultiLineUpwards(e, this._findInput.getValue(), this._findInput.domNode.querySelector('textarea')); + } + + if (e.equals(KeyCode.DownArrow)) { + return stopPropagationForMultiLineDownwards(e, this._findInput.getValue(), this._findInput.domNode.querySelector('textarea')); + } } private _onReplaceInputKeyDown(e: IKeyboardEvent): void { + if (e.equals(KeyMod.WinCtrl | KeyCode.Enter)) { + if (platform.isWindows && platform.isNative && !this._ctrlEnterReplaceAllWarningPrompted) { + // this is the first time when users press Ctrl + Enter to replace all + this._notificationService.info( + nls.localize('ctrlEnter.keybindingChanged', + 'Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior.') + ); - if (e.equals(KeyCode.Enter)) { - this._controller.replace(); - e.preventDefault(); - return; - } + this._ctrlEnterReplaceAllWarningPrompted = true; + this._storageService.store(ctrlEnterReplaceAllWarningPromptedKey, true, StorageScope.GLOBAL); - if (e.equals(KeyMod.CtrlCmd | KeyCode.Enter)) { - this._controller.replaceAll(); - e.preventDefault(); - return; + } + + const inputElement = this._replaceInput.inputBox.inputElement; + const start = inputElement.selectionStart; + const end = inputElement.selectionEnd; + const content = inputElement.value; + + if (start && end) { + const value = content.substr(0, start) + '\n' + content.substr(end); + this._replaceInput.inputBox.value = value; + inputElement.setSelectionRange(start + 1, start + 1); + this._replaceInput.inputBox.layout(); + e.preventDefault(); + return; + } } if (e.equals(KeyCode.Tab)) { @@ -761,6 +862,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas e.preventDefault(); return; } + + if (e.equals(KeyCode.UpArrow)) { + return stopPropagationForMultiLineUpwards(e, this._replaceInput.inputBox.value, this._replaceInput.inputBox.element.querySelector('textarea')); + } + + if (e.equals(KeyCode.DownArrow)) { + return stopPropagationForMultiLineDownwards(e, this._replaceInput.inputBox.value, this._replaceInput.inputBox.element.querySelector('textarea')); + } } // ----- sash @@ -785,6 +894,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } private _buildDomNode(): void { + const flexibleHeight = true; + const flexibleWidth = true; // Find input this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewProvider, { width: FIND_INPUT_AREA_WIDTH, @@ -804,7 +915,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } catch (e) { return { content: e.message }; } - } + }, + flexibleHeight, + flexibleWidth, + flexibleMaxHeight: 118 }, this._contextKeyService, true)); this._findInput.setRegex(!!this._state.isRegex); this._findInput.setCaseSensitive(!!this._state.matchCase); @@ -826,11 +940,24 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._register(this._findInput.onCaseSensitiveKeyDown((e) => { if (e.equals(KeyMod.Shift | KeyCode.Tab)) { if (this._isReplaceVisible) { - this._replaceInputBox.focus(); + this._replaceInput.focus(); + e.preventDefault(); + } + } + })); + this._register(this._findInput.onRegexKeyDown((e) => { + if (e.equals(KeyCode.Tab)) { + if (this._isReplaceVisible) { + this._replaceInput.focusOnPreserve(); e.preventDefault(); } } })); + this._register(this._findInput.inputBox.onDidHeightChange((e) => { + if (this._tryUpdateHeight()) { + this._showViewZone(); + } + })); if (platform.isLinux) { this._register(this._findInput.onMouseDown((e) => this._onFindInputMouseDown(e))); } @@ -860,13 +987,16 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas let findPart = document.createElement('div'); findPart.className = 'find-part'; findPart.appendChild(this._findInput.domNode); - findPart.appendChild(this._matchesCount); - findPart.appendChild(this._prevBtn.domNode); - findPart.appendChild(this._nextBtn.domNode); + const actionsContainer = document.createElement('div'); + actionsContainer.className = 'find-actions'; + findPart.appendChild(actionsContainer); + actionsContainer.appendChild(this._matchesCount); + actionsContainer.appendChild(this._prevBtn.domNode); + actionsContainer.appendChild(this._nextBtn.domNode); // Toggle selection button this._toggleSelectionFind = this._register(new SimpleCheckbox({ - parent: findPart, + parent: actionsContainer, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), onChange: () => { if (this._toggleSelectionFind.checked) { @@ -906,34 +1036,45 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } })); - findPart.appendChild(this._closeBtn.domNode); + actionsContainer.appendChild(this._closeBtn.domNode); // Replace input - let replaceInput = document.createElement('div'); - replaceInput.className = 'replace-input'; - replaceInput.style.width = REPLACE_INPUT_AREA_WIDTH + 'px'; - this._replaceInputBox = this._register(new ContextScopedHistoryInputBox(replaceInput, undefined, { - ariaLabel: NLS_REPLACE_INPUT_LABEL, + this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { + label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, - history: [] - }, this._contextKeyService)); - - - this._register(dom.addStandardDisposableListener(this._replaceInputBox.inputElement, 'keydown', (e) => this._onReplaceInputKeyDown(e))); - this._register(this._replaceInputBox.onDidChange(() => { - this._state.change({ replaceString: this._replaceInputBox.value }, false); + history: [], + flexibleHeight, + flexibleWidth, + flexibleMaxHeight: 118 + }, this._contextKeyService, true)); + this._replaceInput.setPreserveCase(!!this._state.preserveCase); + this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e))); + this._register(this._replaceInput.inputBox.onDidChange(() => { + this._state.change({ replaceString: this._replaceInput.inputBox.value }, false); })); - - this._preserveCase = this._register(new Checkbox({ - actionClassName: 'monaco-preserve-case', - title: NLS_PRESERVE_CASE_LABEL, - isChecked: false, + this._register(this._replaceInput.inputBox.onDidHeightChange((e) => { + if (this._isReplaceVisible && this._tryUpdateHeight()) { + this._showViewZone(); + } })); - this._preserveCase.checked = !!this._state.preserveCase; - this._register(this._preserveCase.onChange(viaKeyboard => { - if (!viaKeyboard) { - this._state.change({ preserveCase: !this._state.preserveCase }, false); - this._replaceInputBox.focus(); + this._register(this._replaceInput.onDidOptionChange(() => { + this._state.change({ + preserveCase: this._replaceInput.getPreserveCase() + }, true); + })); + this._register(this._replaceInput.onPreserveCaseKeyDown((e) => { + if (e.equals(KeyCode.Tab)) { + if (this._prevBtn.isEnabled()) { + this._prevBtn.focus(); + } else if (this._nextBtn.isEnabled()) { + this._nextBtn.focus(); + } else if (this._toggleSelectionFind.isEnabled()) { + this._toggleSelectionFind.focus(); + } else if (this._closeBtn.isEnabled()) { + this._closeBtn.focus(); + } + + e.preventDefault(); } })); @@ -961,17 +1102,16 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } })); - let controls = document.createElement('div'); - controls.className = 'controls'; - controls.style.display = 'block'; - controls.appendChild(this._preserveCase.domNode); - replaceInput.appendChild(controls); - let replacePart = document.createElement('div'); replacePart.className = 'replace-part'; - replacePart.appendChild(replaceInput); - replacePart.appendChild(this._replaceBtn.domNode); - replacePart.appendChild(this._replaceAllBtn.domNode); + replacePart.appendChild(this._replaceInput.domNode); + + const replaceActionsContainer = document.createElement('div'); + replaceActionsContainer.className = 'replace-actions'; + replacePart.appendChild(replaceActionsContainer); + + replaceActionsContainer.appendChild(this._replaceBtn.domNode); + replaceActionsContainer.appendChild(this._replaceAllBtn.domNode); // Toggle replace button this._toggleReplaceBtn = this._register(new SimpleButton({ @@ -980,7 +1120,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas onTrigger: () => { this._state.change({ isReplaceRevealed: !this._isReplaceVisible }, false); if (this._isReplaceVisible) { - this._replaceInputBox.width = this._findInput.inputBox.width; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); + this._replaceInput.inputBox.layout(); } this._showViewZone(); } @@ -1023,9 +1164,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas return; } this._domNode.style.width = `${width}px`; + this._findInput.inputBox.width = inputBoxWidth; if (this._isReplaceVisible) { - this._replaceInputBox.width = inputBoxWidth; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); } + + this._findInput.inputBox.layout(); + this._tryUpdateHeight(); })); } @@ -1085,6 +1230,10 @@ class SimpleCheckbox extends Widget { return this._domNode; } + public isEnabled(): boolean { + return (this._domNode.tabIndex >= 0); + } + public get checked(): boolean { return this._checkbox.checked; } @@ -1094,7 +1243,7 @@ class SimpleCheckbox extends Widget { } public focus(): void { - this._checkbox.focus(); + this._domNode.focus(); } private enable(): void { @@ -1253,4 +1402,11 @@ registerThemingParticipant((theme, collector) => { if (inputActiveBackground) { collector.addRule(`.monaco-editor .find-widget .monaco-checkbox .checkbox:checked + .label { background-color: ${inputActiveBackground.toString()}; }`); } + + // This rule is used to override the outline color for synthetic-focus find input. + const focusOutline = theme.getColor(focusBorder); + if (focusOutline) { + collector.addRule(`.monaco-workbench .monaco-editor .find-widget .monaco-inputbox.synthetic-focus { outline-color: ${focusOutline}; }`); + + } }); diff --git a/src/vs/editor/contrib/find/replacePattern.ts b/src/vs/editor/contrib/find/replacePattern.ts index 50e90d7f3eac6..12a05d93536fe 100644 --- a/src/vs/editor/contrib/find/replacePattern.ts +++ b/src/vs/editor/contrib/find/replacePattern.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from 'vs/base/common/charCode'; -import { containsUppercaseCharacter } from 'vs/base/common/strings'; +import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; const enum ReplacePatternKind { StaticValue = 0, @@ -51,17 +51,8 @@ export class ReplacePattern { public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string { if (this._state.kind === ReplacePatternKind.StaticValue) { - if (preserveCase && matches && (matches[0] !== '')) { - if (matches[0].toUpperCase() === matches[0]) { - return this._state.staticValue.toUpperCase(); - } else if (matches[0].toLowerCase() === matches[0]) { - return this._state.staticValue.toLowerCase(); - } else if (containsUppercaseCharacter(matches[0][0])) { - return this._state.staticValue[0].toUpperCase() + this._state.staticValue.substr(1); - } else { - // we don't understand its pattern yet. - return this._state.staticValue; - } + if (preserveCase) { + return buildReplaceStringWithCasePreserved(matches, this._state.staticValue); } else { return this._state.staticValue; } diff --git a/src/vs/editor/contrib/find/test/replacePattern.test.ts b/src/vs/editor/contrib/find/test/replacePattern.test.ts index d252ac74c6aa3..8a651279a6dfe 100644 --- a/src/vs/editor/contrib/find/test/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/replacePattern.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { ReplacePattern, ReplacePiece, parseReplaceString } from 'vs/editor/contrib/find/replacePattern'; +import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; suite('Replace Pattern test', () => { @@ -154,6 +155,36 @@ suite('Replace Pattern test', () => { assert.equal(actual, 'a{}'); }); + test('buildReplaceStringWithCasePreserved test', () => { + let replacePattern = 'Def'; + let actual: string | string[] = 'abc'; + + assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'def'); + actual = 'Abc'; + assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'Def'); + actual = 'ABC'; + assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'DEF'); + + actual = ['abc', 'Abc']; + assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'def'); + actual = ['Abc', 'abc']; + assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def'); + actual = ['ABC', 'abc']; + assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'DEF'); + + actual = ['AbC']; + assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def'); + actual = ['aBC']; + assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def'); + + actual = ['Foo-Bar']; + assert.equal(buildReplaceStringWithCasePreserved(actual, 'newfoo-newbar'), 'Newfoo-Newbar'); + actual = ['Foo-Bar-Abc']; + assert.equal(buildReplaceStringWithCasePreserved(actual, 'newfoo-newbar-newabc'), 'Newfoo-Newbar-Newabc'); + actual = ['Foo-Bar-abc']; + assert.equal(buildReplaceStringWithCasePreserved(actual, 'newfoo-newbar'), 'Newfoo-newbar'); + }); + test('preserve case', () => { let replacePattern = parseReplaceString('Def'); let actual = replacePattern.buildReplaceString(['abc'], true); @@ -174,5 +205,17 @@ suite('Replace Pattern test', () => { assert.equal(actual, 'Def'); actual = replacePattern.buildReplaceString(['aBC'], true); assert.equal(actual, 'Def'); + + replacePattern = parseReplaceString('newfoo-newbar'); + actual = replacePattern.buildReplaceString(['Foo-Bar'], true); + assert.equal(actual, 'Newfoo-Newbar'); + + replacePattern = parseReplaceString('newfoo-newbar-newabc'); + actual = replacePattern.buildReplaceString(['Foo-Bar-Abc'], true); + assert.equal(actual, 'Newfoo-Newbar-Newabc'); + + replacePattern = parseReplaceString('newfoo-newbar'); + actual = replacePattern.buildReplaceString(['Foo-Bar-abc'], true); + assert.equal(actual, 'Newfoo-newbar'); }); }); diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index f13ed98e9af08..68a3a366faad9 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -105,7 +105,11 @@ export class RangesCollector { } -interface PreviousRegion { indent: number; line: number; marker: boolean; } +interface PreviousRegion { + indent: number; // indent or -2 if a marker + endAbove: number; // end line number for the region above + line: number; // start line of the region. Only used for marker regions. +} export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, foldingRangesLimit = MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT): FoldingRegions { const tabSize = model.getOptions().tabSize; @@ -117,16 +121,19 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol } let previousRegions: PreviousRegion[] = []; - previousRegions.push({ indent: -1, line: model.getLineCount() + 1, marker: false }); // sentinel, to make sure there's at least one entry + let line = model.getLineCount() + 1; + previousRegions.push({ indent: -1, endAbove: line, line }); // sentinel, to make sure there's at least one entry for (let line = model.getLineCount(); line > 0; line--) { let lineContent = model.getLineContent(line); let indent = TextModel.computeIndentLevel(lineContent, tabSize); let previous = previousRegions[previousRegions.length - 1]; if (indent === -1) { - if (offSide && !previous.marker) { - // for offSide languages, empty lines are associated to the next block - previous.line = line; + if (offSide) { + // for offSide languages, empty lines are associated to the previous block + // note: the next block is already written to the results, so this only + // impacts the end position of the block before + previous.endAbove = line; } continue; // only whitespace } @@ -136,7 +143,7 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol if (m[1]) { // start pattern match // discard all regions until the folding pattern let i = previousRegions.length - 1; - while (i > 0 && !previousRegions[i].marker) { + while (i > 0 && previousRegions[i].indent !== -2) { i--; } if (i > 0) { @@ -145,15 +152,15 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol // new folding range from pattern, includes the end line result.insertFirst(line, previous.line, indent); - previous.marker = false; - previous.indent = indent; previous.line = line; + previous.indent = indent; + previous.endAbove = line; continue; } else { // no end marker found, treat line as a regular line } } else { // end pattern match - previousRegions.push({ indent: -2, line, marker: true }); + previousRegions.push({ indent: -2, endAbove: line, line }); continue; } } @@ -165,16 +172,16 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol } while (previous.indent > indent); // new folding range - let endLineNumber = previous.line - 1; + let endLineNumber = previous.endAbove - 1; if (endLineNumber - line >= 1) { // needs at east size 1 result.insertFirst(line, endLineNumber, indent); } } if (previous.indent === indent) { - previous.line = line; + previous.endAbove = line; } else { // previous.indent < indent // new region with a bigger indent - previousRegions.push({ indent, line, marker: false }); + previousRegions.push({ indent, endAbove: line, line }); } } return result.toIndentRanges(model); diff --git a/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts b/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts index 882c097104dc6..07e2025666796 100644 --- a/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts +++ b/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts @@ -316,5 +316,17 @@ suite('Folding with regions', () => { /* 8*/ '#endregionff', ], [], true, markers); }); - + test('Issue 79359', () => { + assertRanges([ + /* 1*/ '#region', + /* 2*/ '', + /* 3*/ 'class A', + /* 4*/ ' foo', + /* 5*/ '', + /* 6*/ 'class A', + /* 7*/ ' foo', + /* 8*/ '', + /* 9*/ '#endregion', + ], [r(1, 9, -1, true), r(3, 4, 0), r(6, 7, 0)], true, markers); + }); }); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 53ea1177390b9..f9f19e5211caf 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -299,10 +299,12 @@ class LinkDetector implements editorCommon.IEditorContribution { return this.openerService.open(uri, { openToSide }); }, err => { + const messageOrError = + err instanceof Error ? (err).message : err; // different error cases - if (err === 'invalid') { + if (messageOrError === 'invalid') { this.notificationService.warn(nls.localize('invalid.url', 'Failed to open this link because it is not well-formed: {0}', link.url!.toString())); - } else if (err === 'missing') { + } else if (messageOrError === 'missing') { this.notificationService.warn(nls.localize('missing.url', 'Failed to open this link because its target is missing.')); } else { onUnexpectedError(err); diff --git a/src/vs/editor/contrib/markdown/markdownRenderer.ts b/src/vs/editor/contrib/markdown/markdownRenderer.ts index 5825a919bf5a8..81f0b2ccc8714 100644 --- a/src/vs/editor/contrib/markdown/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdown/markdownRenderer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { renderMarkdown, RenderOptions } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown, MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; import { URI } from 'vs/base/common/uri'; @@ -33,7 +33,7 @@ export class MarkdownRenderer extends Disposable { super(); } - private getOptions(disposeables: DisposableStore): RenderOptions { + private getOptions(disposeables: DisposableStore): MarkdownRenderOptions { return { codeBlockRenderer: (languageAlias, value) => { // In markdown, diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.ts b/src/vs/editor/contrib/parameterHints/parameterHints.ts index eb846502b6383..0401170929299 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHints.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -18,7 +18,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import * as modes from 'vs/editor/common/modes'; import { TriggerContext } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; -class ParameterHintsController implements IEditorContribution { +class ParameterHintsController extends Disposable implements IEditorContribution { private static readonly ID = 'editor.controller.parameterHints'; @@ -30,8 +30,9 @@ class ParameterHintsController implements IEditorContribution { private readonly widget: ParameterHintsWidget; constructor(editor: ICodeEditor, @IInstantiationService instantiationService: IInstantiationService) { + super(); this.editor = editor; - this.widget = instantiationService.createInstance(ParameterHintsWidget, this.editor); + this.widget = this._register(instantiationService.createInstance(ParameterHintsWidget, this.editor)); } getId(): string { @@ -53,10 +54,6 @@ class ParameterHintsController implements IEditorContribution { trigger(context: TriggerContext): void { this.widget.trigger(context); } - - dispose(): void { - dispose(this.widget); - } } export class TriggerParameterHintsAction extends EditorAction { @@ -76,7 +73,7 @@ export class TriggerParameterHintsAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let controller = ParameterHintsController.get(editor); + const controller = ParameterHintsController.get(editor); if (controller) { controller.trigger({ triggerKind: modes.SignatureHelpTriggerKind.Invoke diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts b/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts index c34c68a84a21b..21cf0393170ea 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts @@ -52,7 +52,7 @@ export class ParameterHintsModel extends Disposable { public readonly onChangedHints = this._onChangedHints.event; private readonly editor: ICodeEditor; - private enabled: boolean; + private triggerOnType = false; private _state: ParameterHintState.State = ParameterHintState.Default; private readonly _lastSignatureHelpResult = this._register(new MutableDisposable()); private triggerChars = new CharacterSet(); @@ -68,7 +68,6 @@ export class ParameterHintsModel extends Disposable { super(); this.editor = editor; - this.enabled = false; this.throttledDelayer = new Delayer(delay); @@ -242,7 +241,7 @@ export class ParameterHintsModel extends Disposable { } private onDidType(text: string) { - if (!this.enabled) { + if (!this.triggerOnType) { return; } @@ -272,9 +271,9 @@ export class ParameterHintsModel extends Disposable { } private onEditorConfigurationChange(): void { - this.enabled = this.editor.getConfiguration().contribInfo.parameterHints.enabled; + this.triggerOnType = this.editor.getConfiguration().contribInfo.parameterHints.enabled; - if (!this.enabled) { + if (!this.triggerOnType) { this.cancel(); } } @@ -283,4 +282,4 @@ export class ParameterHintsModel extends Disposable { this.cancel(true); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/quickOpen/quickOpen.ts b/src/vs/editor/contrib/quickOpen/quickOpen.ts index dba67c40b01dd..2730114175b26 100644 --- a/src/vs/editor/contrib/quickOpen/quickOpen.ts +++ b/src/vs/editor/contrib/quickOpen/quickOpen.ts @@ -3,44 +3,41 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { illegalArgument } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; -import { DocumentSymbol, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { DocumentSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { values } from 'vs/base/common/collections'; -export function getDocumentSymbols(model: ITextModel, flat: boolean, token: CancellationToken): Promise { +export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise { - let roots: DocumentSymbol[] = []; - - let promises = DocumentSymbolProviderRegistry.all(model).map(support => { - - return Promise.resolve(support.provideDocumentSymbols(model, token)).then(result => { - if (Array.isArray(result)) { - roots.push(...result); - } - }, err => { - onUnexpectedExternalError(err); - }); - }); - - return Promise.all(promises).then(() => { - let flatEntries: DocumentSymbol[] = []; - if (token.isCancellationRequested) { - return flatEntries; - } - if (flat) { - flatten(flatEntries, roots, ''); + const model = await OutlineModel.create(document, token); + const roots: DocumentSymbol[] = []; + for (const child of values(model.children)) { + if (child instanceof OutlineElement) { + roots.push(child.symbol); } else { - flatEntries = roots; + roots.push(...values(child.children).map(child => child.symbol)); } - flatEntries.sort(compareEntriesUsingStart); + } + + let flatEntries: DocumentSymbol[] = []; + if (token.isCancellationRequested) { return flatEntries; - }); + } + if (flat) { + flatten(flatEntries, roots, ''); + } else { + flatEntries = roots; + } + + return flatEntries.sort(compareEntriesUsingStart); } function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number { @@ -51,6 +48,7 @@ function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideCo for (let entry of entries) { bucket.push({ kind: entry.kind, + tags: entry.tags, name: entry.name, detail: entry.detail, containerName: entry.containerName || overrideContainerLabel, diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 689b5567ed17b..63aa8a230eb92 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -489,21 +489,18 @@ export class SnippetSession { return; } - const model = this._editor.getModel(); - // make insert edit and start with first selections const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText); this._snippets = snippets; - const selections = model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => { + this._editor.executeEdits('snippet', edits, undoEdits => { if (this._snippets[0].hasPlaceholder) { return this._move(true); } else { return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); } - })!; - this._editor.setSelections(selections); - this._editor.revealRange(selections[0]); + }); + this._editor.revealRange(this._editor.getSelections()[0]); } merge(template: string, options: ISnippetSessionInsertOptions = _defaultOptions): void { @@ -513,8 +510,7 @@ export class SnippetSession { this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText); - this._editor.setSelections(this._editor.getModel().pushEditOperations(this._editor.getSelections(), edits, undoEdits => { - + this._editor.executeEdits('snippet', edits, undoEdits => { for (const snippet of this._snippets) { snippet.merge(snippets); } @@ -525,7 +521,7 @@ export class SnippetSession { } else { return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); } - })!); + }); } next(): void { diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index b56f15882dcf7..a1c080d2b4537 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -27,6 +27,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'CURRENT_DAY_NAME_SHORT': true, 'CURRENT_MONTH_NAME': true, 'CURRENT_MONTH_NAME_SHORT': true, + 'CURRENT_SECONDS_UNIX': true, 'SELECTION': true, 'CLIPBOARD': true, 'TM_SELECTED_TEXT': true, @@ -245,6 +246,8 @@ export class TimeBasedVariableResolver implements VariableResolver { return TimeBasedVariableResolver.monthNames[new Date().getMonth()]; } else if (name === 'CURRENT_MONTH_NAME_SHORT') { return TimeBasedVariableResolver.monthNamesShort[new Date().getMonth()]; + } else if (name === 'CURRENT_SECONDS_UNIX') { + return String(Math.floor(Date.now() / 1000)); } return undefined; diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 7a5367085e77f..b4412f1d13006 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -281,6 +281,7 @@ suite('Snippet Variables Resolver', function () { assertVariableResolve3(resolver, 'CURRENT_DAY_NAME_SHORT'); assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME'); assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME_SHORT'); + assertVariableResolve3(resolver, 'CURRENT_SECONDS_UNIX'); }); test('creating snippet - format-condition doesn\'t work #53617', function () { diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 1fc61b2748870..12cb09f4acdf8 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -111,8 +111,8 @@ .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .close { background-image: url('./close-light.svg'); position: absolute; - top: 0px; - right: 0px; + top: 0; + right: 0; margin-right: 5px; } @@ -155,9 +155,16 @@ } /** Styles for each row in the list **/ + +.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.deprecated { + opacity: 0.66; +} +.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.deprecated > .monaco-icon-label-description-container { + text-decoration: line-through; +} + .monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label::before { height: 100%; - } .monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon { @@ -253,7 +260,7 @@ text-overflow: ellipsis; opacity: 0.7; word-break: break-all; - margin: 0px 24px 0 0; + margin: 0 24px 0 0; padding: 4px 0 12px 5px; } diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 958cff7b5340a..7fd1f0f84977f 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -34,6 +34,8 @@ import { IdleValue } from 'vs/base/common/async'; import { isObject } from 'vs/base/common/types'; import { CommitCharacterController } from './suggestCommitCharacters'; +const _sticky = false; // for development purposes only + export class SuggestController implements IEditorContribution { private static readonly ID: string = 'editor.contrib.suggestController'; @@ -47,7 +49,6 @@ export class SuggestController implements IEditorContribution { private readonly _alternatives: IdleValue; private readonly _toDispose = new DisposableStore(); - private readonly _sticky = false; // for development purposes only constructor( private _editor: ICodeEditor, @@ -127,7 +128,7 @@ export class SuggestController implements IEditorContribution { } })); this._toDispose.add(this._editor.onDidBlurEditorWidget(() => { - if (!this._sticky) { + if (!_sticky) { this._model.cancel(); this._model.clear(); } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 57b6ed5638a96..889b27e0bfa01 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -30,7 +30,7 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; -import { CompletionItemKind, completionKindToCssClass } from 'vs/editor/common/modes'; +import { CompletionItemKind, completionKindToCssClass, CompletionItemTag } from 'vs/editor/common/modes'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -38,6 +38,7 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileKind } from 'vs/platform/files/common/files'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { flatten } from 'vs/base/common/arrays'; const expandSuggestionDocsByDefault = false; @@ -60,7 +61,6 @@ export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget. export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); - const colorRegExp = /^(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))$/i; function extractColor(item: CompletionItem, out: string[]): boolean { if (item.completion.label.match(colorRegExp)) { @@ -173,18 +173,18 @@ class Renderer implements IListRenderer } else if (suggestion.kind === CompletionItemKind.File && this._themeService.getIconTheme().hasFileIcons) { // special logic for 'file' completion items data.icon.className = 'icon hide'; - labelOptions.extraClasses = ([] as string[]).concat( + labelOptions.extraClasses = flatten([ getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FILE), getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FILE) - ); + ]); } else if (suggestion.kind === CompletionItemKind.Folder && this._themeService.getIconTheme().hasFolderIcons) { // special logic for 'folder' completion items data.icon.className = 'icon hide'; - labelOptions.extraClasses = ([] as string[]).concat( + labelOptions.extraClasses = flatten([ getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FOLDER), getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FOLDER) - ); + ]); } else { // normal icon data.icon.className = 'icon hide'; @@ -193,6 +193,11 @@ class Renderer implements IListRenderer ]; } + if (suggestion.tags && suggestion.tags.indexOf(CompletionItemTag.Deprecated) >= 0) { + labelOptions.extraClasses = (labelOptions.extraClasses || []).concat(['deprecated']); + labelOptions.matches = []; + } + data.iconLabel.setLabel(suggestion.label, undefined, labelOptions); data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, ''); diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index 2e7073d55a696..703706bf238bf 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -51,7 +51,7 @@ const WIDGET_ID = 'vs.editor.contrib.zoneWidget'; export class ViewZoneDelegate implements IViewZone { public domNode: HTMLElement; - public id: number = 0; // A valid zone id should be greater than 0 + public id: string = ''; // A valid zone id should be greater than 0 public afterLineNumber: number; public afterColumn: number; public heightInLines: number; @@ -192,9 +192,6 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { } public dispose(): void { - - this._disposables.dispose(); - if (this._overlayWidget) { this.editor.removeOverlayWidget(this._overlayWidget); this._overlayWidget = null; @@ -211,6 +208,8 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { this.editor.deltaDecorations(this._positionMarkerId, []); this._positionMarkerId = []; + + this._disposables.dispose(); } public create(): void { diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index 22e860f8eb0f7..1981153c0c858 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -7,7 +7,7 @@ import 'vs/css!./accessibilityHelp'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { renderFormattedText } from 'vs/base/browser/htmlContentRenderer'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Widget } from 'vs/base/browser/ui/widget'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 54804eb3c0f93..b042afbc213cc 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -45,6 +45,7 @@ import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platf import { ILayoutService, IDimension } from 'vs/platform/layout/browser/layoutService'; import { SimpleServicesNLS } from 'vs/editor/common/standaloneStrings'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; +import { basename } from 'vs/base/common/resources'; export class SimpleModel implements IResolvedTextEditorModel { @@ -656,6 +657,7 @@ export class SimpleBulkEditService implements IBulkEditService { } export class SimpleUriLabelService implements ILabelService { + _serviceBrand: any; private readonly _onDidRegisterFormatter = new Emitter(); @@ -668,6 +670,10 @@ export class SimpleUriLabelService implements ILabelService { return resource.path; } + getUriBasenameLabel(resource: URI): string { + return basename(resource); + } + public getWorkspaceLabel(workspace: IWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { return ''; } diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 31a3f56b7042c..d64e0e6e2afbb 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -21,7 +21,7 @@ import { IMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/acti import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -29,6 +29,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { StandaloneCodeEditorNLS } from 'vs/editor/common/standaloneStrings'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; /** * Description of an action contribution @@ -373,7 +374,9 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon @ICodeEditorService codeEditorService: ICodeEditorService, @IStandaloneThemeService themeService: IStandaloneThemeService, @INotificationService notificationService: INotificationService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IClipboardService clipboardService: IClipboardService ) { applyConfigurationValues(configurationService, options, true); options = options || {}; @@ -381,7 +384,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon options.theme = themeService.setTheme(options.theme); } - super(domElement, options, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService); + super(domElement, options, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, clipboardService); this._contextViewService = contextViewService; this._configurationService = configurationService; diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 8750db8f94301..280e00cbdf712 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -30,7 +30,7 @@ import { IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standal import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarker, IMarkerData } from 'vs/platform/markers/common/markers'; @@ -38,6 +38,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { clearAllFontInfos } from 'vs/editor/browser/config/configuration'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; type Omit = Pick>; @@ -119,6 +120,8 @@ export function createDiffEditor(domElement: HTMLElement, options?: IDiffEditorC services.get(IStandaloneThemeService), services.get(INotificationService), services.get(IConfigurationService), + services.get(IContextMenuService), + services.get(IClipboardService) ); }); } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index e8e9dffbe8403..1818394dfdc02 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -562,8 +562,10 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { // enums DocumentHighlightKind: standaloneEnums.DocumentHighlightKind, CompletionItemKind: standaloneEnums.CompletionItemKind, + CompletionItemTag: standaloneEnums.CompletionItemTag, CompletionItemInsertTextRule: standaloneEnums.CompletionItemInsertTextRule, SymbolKind: standaloneEnums.SymbolKind, + SymbolTag: standaloneEnums.SymbolTag, IndentAction: standaloneEnums.IndentAction, CompletionTriggerKind: standaloneEnums.CompletionTriggerKind, SignatureHelpTriggerKind: standaloneEnums.SignatureHelpTriggerKind, diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index f056854e44aab..2c98c6ba429be 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -27,11 +27,9 @@ function isArrayOf(elemType: (x: any) => boolean, obj: any): boolean { if (!(Array.isArray(obj))) { return false; } - for (let idx in obj) { - if (obj.hasOwnProperty(idx)) { - if (!(elemType(obj[idx]))) { - return false; - } + for (const el of obj) { + if (!(elemType(el))) { + return false; } } return true; @@ -298,10 +296,8 @@ function compileAction(lexer: monarchCommon.ILexerMin, ruleName: string, action: } else if (Array.isArray(action)) { let results: monarchCommon.FuzzyAction[] = []; - for (let idx in action) { - if (action.hasOwnProperty(idx)) { - results[idx] = compileAction(lexer, ruleName, action[idx]); - } + for (let i = 0, len = action.length; i < len; i++) { + results[i] = compileAction(lexer, ruleName, action[i]); } return { group: results }; } @@ -331,13 +327,10 @@ function compileAction(lexer: monarchCommon.ILexerMin, ruleName: string, action: const def = lexer.defaultToken; return { test: function (id, matches, state, eos) { - for (let idx in cases) { - if (cases.hasOwnProperty(idx)) { - const _case = cases[idx]; - const didmatch = (!_case.test || _case.test(id, matches, state, eos)); - if (didmatch) { - return _case.value; - } + for (const _case of cases) { + const didmatch = (!_case.test || _case.test(id, matches, state, eos)); + if (didmatch) { + return _case.value; } } return def; @@ -425,64 +418,61 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // Compile an array of rules into newrules where RegExp objects are created. function addRules(state: string, newrules: monarchCommon.IRule[], rules: any[]) { - for (let idx in rules) { - if (rules.hasOwnProperty(idx)) { - const rule = rules[idx]; - let include = rule.include; - if (include) { - if (typeof (include) !== 'string') { - throw monarchCommon.createError(lexer, 'an \'include\' attribute must be a string at: ' + state); - } - if (include[0] === '@') { - include = include.substr(1); // peel off starting @ - } - if (!json.tokenizer[include]) { - throw monarchCommon.createError(lexer, 'include target \'' + include + '\' is not defined at: ' + state); - } - addRules(state + '.' + include, newrules, json.tokenizer[include]); - } - else { - const newrule = new Rule(state); + for (const rule of rules) { - - // Set up new rule attributes - if (Array.isArray(rule) && rule.length >= 1 && rule.length <= 3) { - newrule.setRegex(lexerMin, rule[0]); - if (rule.length >= 3) { - if (typeof (rule[1]) === 'string') { - newrule.setAction(lexerMin, { token: rule[1], next: rule[2] }); - } - else if (typeof (rule[1]) === 'object') { - const rule1 = rule[1]; - rule1.next = rule[2]; - newrule.setAction(lexerMin, rule1); - } - else { - throw monarchCommon.createError(lexer, 'a next state as the last element of a rule can only be given if the action is either an object or a string, at: ' + state); - } + let include = rule.include; + if (include) { + if (typeof (include) !== 'string') { + throw monarchCommon.createError(lexer, 'an \'include\' attribute must be a string at: ' + state); + } + if (include[0] === '@') { + include = include.substr(1); // peel off starting @ + } + if (!json.tokenizer[include]) { + throw monarchCommon.createError(lexer, 'include target \'' + include + '\' is not defined at: ' + state); + } + addRules(state + '.' + include, newrules, json.tokenizer[include]); + } + else { + const newrule = new Rule(state); + + // Set up new rule attributes + if (Array.isArray(rule) && rule.length >= 1 && rule.length <= 3) { + newrule.setRegex(lexerMin, rule[0]); + if (rule.length >= 3) { + if (typeof (rule[1]) === 'string') { + newrule.setAction(lexerMin, { token: rule[1], next: rule[2] }); + } + else if (typeof (rule[1]) === 'object') { + const rule1 = rule[1]; + rule1.next = rule[2]; + newrule.setAction(lexerMin, rule1); } else { - newrule.setAction(lexerMin, rule[1]); + throw monarchCommon.createError(lexer, 'a next state as the last element of a rule can only be given if the action is either an object or a string, at: ' + state); } } else { - if (!rule.regex) { - throw monarchCommon.createError(lexer, 'a rule must either be an array, or an object with a \'regex\' or \'include\' field at: ' + state); - } - if (rule.name) { - if (typeof rule.name === 'string') { - newrule.name = rule.name; - } - } - if (rule.matchOnlyAtStart) { - newrule.matchOnlyAtLineStart = bool(rule.matchOnlyAtLineStart, false); + newrule.setAction(lexerMin, rule[1]); + } + } + else { + if (!rule.regex) { + throw monarchCommon.createError(lexer, 'a rule must either be an array, or an object with a \'regex\' or \'include\' field at: ' + state); + } + if (rule.name) { + if (typeof rule.name === 'string') { + newrule.name = rule.name; } - newrule.setRegex(lexerMin, rule.regex); - newrule.setAction(lexerMin, rule.action); } - - newrules.push(newrule); + if (rule.matchOnlyAtStart) { + newrule.matchOnlyAtLineStart = bool(rule.matchOnlyAtLineStart, false); + } + newrule.setRegex(lexerMin, rule.regex); + newrule.setAction(lexerMin, rule.action); } + + newrules.push(newrule); } } } @@ -520,26 +510,24 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm { open: '<', close: '>', token: 'delimiter.angle' }]; } let brackets: IMonarchLanguageBracket[] = []; - for (let bracketIdx in json.brackets) { - if (json.brackets.hasOwnProperty(bracketIdx)) { - let desc = json.brackets[bracketIdx]; - if (desc && Array.isArray(desc) && desc.length === 3) { - desc = { token: desc[2], open: desc[0], close: desc[1] }; - } - if (desc.open === desc.close) { - throw monarchCommon.createError(lexer, 'open and close brackets in a \'brackets\' attribute must be different: ' + desc.open + - '\n hint: use the \'bracket\' attribute if matching on equal brackets is required.'); - } - if (typeof desc.open === 'string' && typeof desc.token === 'string' && typeof desc.close === 'string') { - brackets.push({ - token: desc.token + lexer.tokenPostfix, - open: monarchCommon.fixCase(lexer, desc.open), - close: monarchCommon.fixCase(lexer, desc.close) - }); - } - else { - throw monarchCommon.createError(lexer, 'every element in the \'brackets\' array must be a \'{open,close,token}\' object or array'); - } + for (let el of json.brackets) { + let desc: any = el; + if (desc && Array.isArray(desc) && desc.length === 3) { + desc = { token: desc[2], open: desc[0], close: desc[1] }; + } + if (desc.open === desc.close) { + throw monarchCommon.createError(lexer, 'open and close brackets in a \'brackets\' attribute must be different: ' + desc.open + + '\n hint: use the \'bracket\' attribute if matching on equal brackets is required.'); + } + if (typeof desc.open === 'string' && typeof desc.token === 'string' && typeof desc.close === 'string') { + brackets.push({ + token: desc.token + lexer.tokenPostfix, + open: monarchCommon.fixCase(lexer, desc.open), + close: monarchCommon.fixCase(lexer, desc.close) + }); + } + else { + throw monarchCommon.createError(lexer, 'every element in the \'brackets\' array must be a \'{open,close,token}\' object or array'); } } lexer.brackets = brackets; diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index a5c97ad6f288c..39642a4d6e60c 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -228,8 +228,6 @@ class MonarchLineState implements modes.IState { } } -const hasOwnProperty = Object.hasOwnProperty; - interface IMonarchTokensCollector { enterMode(startOffset: number, modeId: string): void; emit(startOffset: number, type: string): void; @@ -423,22 +421,24 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { public getLoadStatus(): ILoadStatus { let promises: Thenable[] = []; for (let nestedModeId in this._embeddedModes) { - const tokenizationSupport = modes.TokenizationRegistry.get(nestedModeId); - if (tokenizationSupport) { - // The nested mode is already loaded - if (tokenizationSupport instanceof MonarchTokenizer) { - const nestedModeStatus = tokenizationSupport.getLoadStatus(); - if (nestedModeStatus.loaded === false) { - promises.push(nestedModeStatus.promise); + if (this._embeddedModes.hasOwnProperty(nestedModeId)) { + const tokenizationSupport = modes.TokenizationRegistry.get(nestedModeId); + if (tokenizationSupport) { + // The nested mode is already loaded + if (tokenizationSupport instanceof MonarchTokenizer) { + const nestedModeStatus = tokenizationSupport.getLoadStatus(); + if (nestedModeStatus.loaded === false) { + promises.push(nestedModeStatus.promise); + } } + continue; } - continue; - } - const tokenizationSupportPromise = modes.TokenizationRegistry.getPromise(nestedModeId); - if (tokenizationSupportPromise) { - // The nested mode is in the process of being loaded - promises.push(tokenizationSupportPromise); + const tokenizationSupportPromise = modes.TokenizationRegistry.getPromise(nestedModeId); + if (tokenizationSupportPromise) { + // The nested mode is in the process of being loaded + promises.push(tokenizationSupportPromise); + } } } @@ -490,11 +490,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { let popOffset = -1; let hasEmbeddedPopRule = false; - for (let idx in rules) { - if (!hasOwnProperty.call(rules, idx)) { - continue; - } - let rule: monarchCommon.IRule = rules[idx]; + for (const rule of rules) { if (!monarchCommon.isIAction(rule.action) || rule.action.nextEmbedded !== '@pop') { continue; } @@ -619,16 +615,13 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { // try each rule until we match let restOfLine = line.substr(pos); - for (let idx in rules) { - if (hasOwnProperty.call(rules, idx)) { - let rule: monarchCommon.IRule = rules[idx]; - if (pos === 0 || !rule.matchOnlyAtLineStart) { - matches = restOfLine.match(rule.regex); - if (matches) { - matched = matches[0]; - action = rule.action; - break; - } + for (const rule of rules) { + if (pos === 0 || !rule.matchOnlyAtLineStart) { + matches = restOfLine.match(rule.regex); + if (matches) { + matched = matches[0]; + action = rule.action; + break; } } } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index a42bd6d06c303..59e221a8c2fd7 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -3998,8 +3998,12 @@ suite('autoClosingPairs', () => { { open: '\'', close: '\'', notIn: ['string', 'comment'] }, { open: '\"', close: '\"', notIn: ['string'] }, { open: '`', close: '`', notIn: ['string', 'comment'] }, - { open: '/**', close: ' */', notIn: ['string'] } + { open: '/**', close: ' */', notIn: ['string'] }, + { open: 'begin', close: 'end', notIn: ['string'] } ], + __electricCharacterSupport: { + docComment: { open: '/**', close: ' */' } + } })); } @@ -4439,6 +4443,28 @@ suite('autoClosingPairs', () => { mode.dispose(); }); + test('multi-character autoclose', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + '', + ], + languageIdentifier: mode.getLanguageIdentifier() + }, (model, cursor) => { + + model.setValue('begi'); + cursor.setSelections('test', [new Selection(1, 5, 1, 5)]); + cursorCommand(cursor, H.Type, { text: 'n' }, 'keyboard'); + assert.strictEqual(model.getLineContent(1), 'beginend'); + + model.setValue('/*'); + cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); + cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard'); + assert.strictEqual(model.getLineContent(1), '/** */'); + }); + mode.dispose(); + }); + test('issue #55314: Do not auto-close when ending with open', () => { const languageId = new LanguageIdentifier('myElectricMode', 5); class ElectricMode extends MockMode { @@ -4477,7 +4503,7 @@ suite('autoClosingPairs', () => { model.forceTokenization(model.getLineCount()); assertType(model, cursor, 3, 4, '"', '"', `does not double quote when ending with open`); model.forceTokenization(model.getLineCount()); - assertType(model, cursor, 4, 2, '"', '""', `double quote when ending with open`); + assertType(model, cursor, 4, 2, '"', '"', `does not double quote when ending with open`); model.forceTokenization(model.getLineCount()); assertType(model, cursor, 4, 3, '"', '"', `does not double quote when ending with open`); }); @@ -4692,6 +4718,28 @@ suite('autoClosingPairs', () => { mode.dispose(); }); + test('issue #78975 - Parentheses swallowing does not work when parentheses are inserted by autocomplete', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + '
{ + cursor.setSelections('test', [new Selection(1, 8, 1, 8)]); + + cursor.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)]); + assert.strictEqual(model.getLineContent(1), '
{ let mode = new AutoClosingMode(); usingCursor({ @@ -4750,31 +4798,18 @@ suite('autoClosingPairs', () => { // on the mac US intl kb layout - // Typing ` + space + // Typing ' + space cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); - assert.equal(model.getValue(), '\'\''); - // Typing " + space within string - cursor.setSelections('test', [new Selection(1, 2, 1, 2)]); - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '"' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); - - assert.equal(model.getValue(), '\'"\''); - - // Typing ' + space after ' - model.setValue('\''); - cursor.setSelections('test', [new Selection(1, 2, 1, 2)]); + // Typing one more ' + space cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); - assert.equal(model.getValue(), '\'\''); // Typing ' as a closing tag diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index eadf902357a35..6824acf4d84ed 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -9,12 +9,11 @@ import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands'; suite('OpenerService', function () { - const editorService = new TestCodeEditorService(); - let lastCommand: { id: string, args: any[] } | undefined; + let lastCommand: { id: string; args: any[] } | undefined; - const commandService = new class implements ICommandService { + const commandService = new (class implements ICommandService { _serviceBrand: any; onWillExecuteCommand = () => ({ dispose: () => { } }); onDidExecuteCommand = () => ({ dispose: () => { } }); @@ -22,7 +21,7 @@ suite('OpenerService', function () { lastCommand = { id, args }; return Promise.resolve(undefined); } - }; + })(); setup(function () { lastCommand = undefined; @@ -35,7 +34,6 @@ suite('OpenerService', function () { }); test('delegate to editorService, scheme:///fff#L123', function () { - const openerService = new OpenerService(editorService, NullCommandService); openerService.open(URI.parse('file:///somepath#L23')); @@ -58,7 +56,6 @@ suite('OpenerService', function () { }); test('delegate to editorService, scheme:///fff#123,123', function () { - const openerService = new OpenerService(editorService, NullCommandService); openerService.open(URI.parse('file:///somepath#23')); @@ -77,7 +74,6 @@ suite('OpenerService', function () { }); test('delegate to commandsService, command:someid', function () { - const openerService = new OpenerService(editorService, commandService); const id = `aCommand${Math.random()}`; @@ -98,4 +94,108 @@ suite('OpenerService', function () { assert.equal(lastCommand!.args[0], 12); assert.equal(lastCommand!.args[1], true); }); + + test('links are protected by validators', async function () { + const openerService = new OpenerService(editorService, commandService); + + openerService.registerValidator({ shouldOpen: () => Promise.resolve(false) }); + + const httpResult = await openerService.open(URI.parse('https://www.microsoft.com')); + const httpsResult = await openerService.open(URI.parse('https://www.microsoft.com')); + assert.equal(httpResult, false); + assert.equal(httpsResult, false); + }); + + test('links validated by validators go to openers', async function () { + const openerService = new OpenerService(editorService, commandService); + + openerService.registerValidator({ shouldOpen: () => Promise.resolve(true) }); + + let openCount = 0; + openerService.registerOpener({ + open: (resource: URI) => { + openCount++; + return Promise.resolve(true); + } + }); + + await openerService.open(URI.parse('http://microsoft.com')); + assert.equal(openCount, 1); + await openerService.open(URI.parse('https://microsoft.com')); + assert.equal(openCount, 2); + }); + + test('links validated by multiple validators', async function () { + const openerService = new OpenerService(editorService, commandService); + + let v1 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v1++; + return Promise.resolve(true); + } + }); + + let v2 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v2++; + return Promise.resolve(true); + } + }); + + let openCount = 0; + openerService.registerOpener({ + open: (resource: URI) => { + openCount++; + return Promise.resolve(true); + } + }); + + await openerService.open(URI.parse('http://microsoft.com')); + assert.equal(openCount, 1); + assert.equal(v1, 1); + assert.equal(v2, 1); + await openerService.open(URI.parse('https://microsoft.com')); + assert.equal(openCount, 2); + assert.equal(v1, 2); + assert.equal(v2, 2); + }); + + test('links invalidated by first validator do not continue validating', async function () { + const openerService = new OpenerService(editorService, commandService); + + let v1 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v1++; + return Promise.resolve(false); + } + }); + + let v2 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v2++; + return Promise.resolve(true); + } + }); + + let openCount = 0; + openerService.registerOpener({ + open: (resource: URI) => { + openCount++; + return Promise.resolve(true); + } + }); + + await openerService.open(URI.parse('http://microsoft.com')); + assert.equal(openCount, 0); + assert.equal(v1, 1); + assert.equal(v2, 0); + await openerService.open(URI.parse('https://microsoft.com')); + assert.equal(openCount, 0); + assert.equal(v1, 2); + assert.equal(v2, 0); + }); }); diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index ba9aa15f503b5..3f3f6880450ad 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { StandardTokenType } from 'vs/editor/common/modes'; import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair'; import { TokenText, createFakeScopedLineTokens } from 'vs/editor/test/common/modesTestUtils'; +import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; suite('CharacterPairSupport', () => { @@ -52,8 +53,21 @@ suite('CharacterPairSupport', () => { assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); }); + function findAutoClosingPair(characterPairSupport: CharacterPairSupport, character: string): StandardAutoClosingPairConditional | null { + for (const autoClosingPair of characterPairSupport.getAutoClosingPairs()) { + if (autoClosingPair.open === character) { + return autoClosingPair; + } + } + return null; + } + function testShouldAutoClose(characterPairSupport: CharacterPairSupport, line: TokenText[], character: string, column: number): boolean { - return characterPairSupport.shouldAutoClosePair(character, createFakeScopedLineTokens(line), column); + const autoClosingPair = findAutoClosingPair(characterPairSupport, character); + if (!autoClosingPair) { + return false; + } + return CharacterPairSupport.shouldAutoClosePair(autoClosingPair, createFakeScopedLineTokens(line), column); } test('shouldAutoClosePair in empty line', () => { diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 6035379e05a5d..22b818c6b67e9 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -21,86 +21,20 @@ suite('Editor Modes - Auto Indentation', () => { assert.deepEqual(actual, null); } - function testAppends(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, appendText: string): void { - let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, { appendText: appendText }); - } - function testMatchBracket(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, matchOpenBracket: string): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); assert.deepEqual(actual, { matchOpenBracket: matchOpenBracket }); } - test('Doc comments', () => { - let brackets = new BracketElectricCharacterSupport(null, [{ open: '/**', close: ' */' }], null); - - testAppends(brackets, [ - { text: '/*', type: StandardTokenType.Other }, - ], '*', 3, ' */'); - - testDoesNothing(brackets, [ - { text: '/*', type: StandardTokenType.Other }, - { text: ' ', type: StandardTokenType.Other }, - { text: '*/', type: StandardTokenType.Other }, - ], '*', 3); - }); - test('getElectricCharacters uses all sources and dedups', () => { let sup = new BracketElectricCharacterSupport( new RichEditBrackets(fakeLanguageIdentifier, [ ['{', '}'], ['(', ')'] - ]), [ - { open: '{', close: '}', notIn: ['string', 'comment'] }, - { open: '"', close: '"', notIn: ['string', 'comment'] }, - { open: 'begin', close: 'end', notIn: ['string'] } - ], - { docComment: { open: '/**', close: ' */' } } - ); - - assert.deepEqual(sup.getElectricCharacters(), ['}', ')', 'n', '*']); - }); - - test('auto-close', () => { - let sup = new BracketElectricCharacterSupport( - new RichEditBrackets(fakeLanguageIdentifier, [ - ['{', '}'], - ['(', ')'] - ]), [ - { open: '{', close: '}', notIn: ['string', 'comment'] }, - { open: '"', close: '"', notIn: ['string', 'comment'] }, - { open: 'begin', close: 'end', notIn: ['string'] } - ], - { docComment: { open: '/**', close: ' */' } } + ]) ); - testDoesNothing(sup, [], 'a', 0); - - testDoesNothing(sup, [{ text: 'egi', type: StandardTokenType.Other }], 'b', 1); - testDoesNothing(sup, [{ text: 'bgi', type: StandardTokenType.Other }], 'e', 2); - testDoesNothing(sup, [{ text: 'bei', type: StandardTokenType.Other }], 'g', 3); - testDoesNothing(sup, [{ text: 'beg', type: StandardTokenType.Other }], 'i', 4); - - testDoesNothing(sup, [{ text: 'egin', type: StandardTokenType.Other }], 'b', 1); - testDoesNothing(sup, [{ text: 'bgin', type: StandardTokenType.Other }], 'e', 2); - testDoesNothing(sup, [{ text: 'bein', type: StandardTokenType.Other }], 'g', 3); - testDoesNothing(sup, [{ text: 'begn', type: StandardTokenType.Other }], 'i', 4); - testAppends(sup, [{ text: 'begi', type: StandardTokenType.Other }], 'n', 5, 'end'); - - testDoesNothing(sup, [{ text: '3gin', type: StandardTokenType.Other }], 'b', 1); - testDoesNothing(sup, [{ text: 'bgin', type: StandardTokenType.Other }], '3', 2); - testDoesNothing(sup, [{ text: 'b3in', type: StandardTokenType.Other }], 'g', 3); - testDoesNothing(sup, [{ text: 'b3gn', type: StandardTokenType.Other }], 'i', 4); - testDoesNothing(sup, [{ text: 'b3gi', type: StandardTokenType.Other }], 'n', 5); - - testDoesNothing(sup, [{ text: 'begi', type: StandardTokenType.String }], 'n', 5); - - testAppends(sup, [{ text: '"', type: StandardTokenType.String }, { text: 'begi', type: StandardTokenType.Other }], 'n', 6, 'end'); - testDoesNothing(sup, [{ text: '"', type: StandardTokenType.String }, { text: 'begi', type: StandardTokenType.String }], 'n', 6); - - testAppends(sup, [{ text: '/*', type: StandardTokenType.String }], '*', 3, ' */'); - - testDoesNothing(sup, [{ text: 'begi', type: StandardTokenType.Other }, { text: 'end', type: StandardTokenType.Other }], 'n', 5); + assert.deepEqual(sup.getElectricCharacters(), ['}', ')']); }); test('matchOpenBracket', () => { @@ -108,12 +42,7 @@ suite('Editor Modes - Auto Indentation', () => { new RichEditBrackets(fakeLanguageIdentifier, [ ['{', '}'], ['(', ')'] - ]), [ - { open: '{', close: '}', notIn: ['string', 'comment'] }, - { open: '"', close: '"', notIn: ['string', 'comment'] }, - { open: 'begin', close: 'end', notIn: ['string'] } - ], - { docComment: { open: '/**', close: ' */' } } + ]) ); testDoesNothing(sup, [{ text: '\t{', type: StandardTokenType.Other }], '\t', 1); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a797b72a6f2e4..4291958ce59f8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3605,17 +3605,17 @@ declare namespace monaco.editor { * @param zone Zone to create * @return A unique identifier to the view zone. */ - addZone(zone: IViewZone): number; + addZone(zone: IViewZone): string; /** * Remove a zone * @param id A unique identifier to the view zone, as returned by the `addZone` call. */ - removeZone(id: number): void; + removeZone(id: string): void; /** * Change a zone's position. * The editor will rescan the `afterLineNumber` and `afterColumn` properties of a view zone. */ - layoutZone(id: number): void; + layoutZone(id: string): void; } /** @@ -4054,7 +4054,7 @@ declare namespace monaco.editor { * @param edits The edits to execute. * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; /** * Execute multiple (concomitant) commands on the editor. * @param source The source of the call. @@ -4561,7 +4561,9 @@ declare namespace monaco.languages { * * @deprecated Will be replaced by a better API soon. */ - __electricCharacterSupport?: IBracketElectricCharacterContribution; + __electricCharacterSupport?: { + docComment?: IDocComment; + }; } /** @@ -4636,10 +4638,6 @@ declare namespace monaco.languages { action: EnterAction; } - export interface IBracketElectricCharacterContribution { - docComment?: IDocComment; - } - /** * Definition of documentation comments (e.g. Javadoc/JSdoc) */ @@ -4788,6 +4786,10 @@ declare namespace monaco.languages { Snippet = 25 } + export enum CompletionItemTag { + Deprecated = 1 + } + export enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to @@ -4816,6 +4818,11 @@ declare namespace monaco.languages { * an icon is chosen by the editor. */ kind: CompletionItemKind; + /** + * A modifier to the `kind` which affect how the item + * is rendered, e.g. Deprecated is rendered with a strikeout + */ + tags?: ReadonlyArray; /** * A human-readable string with additional information * about this item, like type or symbol information. @@ -5225,10 +5232,15 @@ declare namespace monaco.languages { TypeParameter = 25 } + export enum SymbolTag { + Deprecated = 1 + } + export interface DocumentSymbol { name: string; detail: string; kind: SymbolKind; + tags: ReadonlyArray; containerName?: string; range: IRange; selectionRange: IRange; diff --git a/src/vs/nls.d.ts b/src/vs/nls.d.ts index 38bbd076068b4..3942ff08669b4 100644 --- a/src/vs/nls.d.ts +++ b/src/vs/nls.d.ts @@ -8,5 +8,12 @@ export interface ILocalizeInfo { comment: string[]; } +/** + * Localize a message. `message` can contain `{n}` notation where it is replaced by the nth value in `...args`. + */ export declare function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string; + +/** + * Localize a message. `message` can contain `{n}` notation where it is replaced by the nth value in `...args`. + */ export declare function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 6c3102bc4df28..01d88e2ece88c 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addClasses, createCSSRule, removeClasses, asDomUri } from 'vs/base/browser/dom'; +import { addClasses, createCSSRule, removeClasses, asCSSUrl } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; import { ActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction } from 'vs/base/common/actions'; @@ -244,8 +244,8 @@ export class MenuEntryActionViewItem extends ActionViewItem { iconClass = MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; } else { iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: url("${asDomUri(item.iconLocation.light || item.iconLocation.dark).toString()}")`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${asDomUri(item.iconLocation.dark).toString()}")`); + createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(item.iconLocation.light || item.iconLocation.dark)}`); + createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(item.iconLocation.dark)}`); MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); } diff --git a/src/vs/platform/browser/contextScopedHistoryWidget.ts b/src/vs/platform/browser/contextScopedHistoryWidget.ts index 5e5e8603683b9..8625db10ee845 100644 --- a/src/vs/platform/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/browser/contextScopedHistoryWidget.ts @@ -10,6 +10,7 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ReplaceInput, IReplaceInputOptions } from 'vs/base/browser/ui/findinput/replaceInput'; export const HistoryNavigationWidgetContext = 'historyNavigationWidget'; export const HistoryNavigationEnablementContext = 'historyNavigationEnabled'; @@ -60,6 +61,16 @@ export class ContextScopedFindInput extends FindInput { super(container, contextViewProvider, showFindOptions, options); this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, { target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService); } +} + +export class ContextScopedReplaceInput extends ReplaceInput { + + constructor(container: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, options: IReplaceInputOptions, + @IContextKeyService contextKeyService: IContextKeyService, showReplaceOptions: boolean = false + ) { + super(container, contextViewProvider, showReplaceOptions, options); + this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, { target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService); + } } diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index 8ea9e8e026df2..c5e8a2676d28f 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -15,10 +15,18 @@ export class BrowserClipboardService implements IClipboardService { private _internalResourcesClipboard: URI[] | undefined; async writeText(text: string, type?: string): Promise { + if (type) { + return; // TODO@sbatten + } + return navigator.clipboard.writeText(text); } async readText(type?: string): Promise { + if (type) { + return ''; // TODO@sbatten + } + return navigator.clipboard.readText(); } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 0d27f890edf91..329276d4e46e1 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -99,6 +99,10 @@ export const enum ConfigurationScope { * Resource specific configuration, which can be configured in the user, workspace or folder settings. */ RESOURCE, + /** + * Machine specific configuration that can also be configured in workspace or folder settings. + */ + MACHINE_OVERRIDABLE, } export interface IConfigurationPropertySchema extends IJSONSchema { diff --git a/src/vs/platform/diagnostics/common/diagnosticsService.ts b/src/vs/platform/diagnostics/common/diagnostics.ts similarity index 69% rename from src/vs/platform/diagnostics/common/diagnosticsService.ts rename to src/vs/platform/diagnostics/common/diagnostics.ts index ae854fcfb7caf..9adfc578f6496 100644 --- a/src/vs/platform/diagnostics/common/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/common/diagnostics.ts @@ -5,8 +5,6 @@ import { UriComponents } from 'vs/base/common/uri'; import { ProcessItem } from 'vs/base/common/processes'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IMainProcessInfo } from 'vs/platform/launch/common/launchService'; import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -63,18 +61,10 @@ export interface PerformanceInfo { workspaceInfo?: string; } -export const ID = 'diagnosticsService'; -export const IDiagnosticsService = createDecorator(ID); - -export interface IDiagnosticsService { - _serviceBrand: any; - - getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise; - getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise; - getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise; - reportWorkspaceStats(workspace: IWorkspace): Promise; +export interface IWorkspaceInformation extends IWorkspace { + telemetryId: string | undefined; } export function isRemoteDiagnosticError(x: any): x is IRemoteDiagnosticError { return !!x.hostName && !!x.errorMessage; -} \ No newline at end of file +} diff --git a/src/vs/platform/diagnostics/node/diagnosticsIpc.ts b/src/vs/platform/diagnostics/node/diagnosticsIpc.ts index 2e2bf87472b4d..76af1758163e2 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsIpc.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsIpc.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IDiagnosticsService, IRemoteDiagnosticInfo, IRemoteDiagnosticError, SystemInfo, PerformanceInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IRemoteDiagnosticInfo, IRemoteDiagnosticError, SystemInfo, PerformanceInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IDiagnosticsService } from './diagnosticsService'; import { Event } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessInfo } from 'vs/platform/launch/common/launchService'; diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 9e74621348c63..1f5326fd136b5 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; -import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, IDiagnosticsService, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; import { readdir, stat, exists, readFile } from 'fs'; import { join, basename } from 'vs/base/common/path'; import { parse, ParseError } from 'vs/base/common/json'; @@ -16,8 +16,20 @@ import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ProcessItem } from 'vs/base/common/processes'; import { IMainProcessInfo } from 'vs/platform/launch/common/launchService'; -import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ID = 'diagnosticsService'; +export const IDiagnosticsService = createDecorator(ID); + +export interface IDiagnosticsService { + _serviceBrand: any; + + getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise; + getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise; + getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise; + reportWorkspaceStats(workspace: IWorkspaceInformation): Promise; +} export interface VersionInfo { vscodeVersion: string; @@ -64,16 +76,27 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise return done(results); } + if (token.count > MAX_FILES) { + token.count += files.length; + token.maxReached = true; + return done(results); + } + let pending = files.length; if (pending === 0) { return done(results); } - for (const file of files) { - if (token.maxReached) { - return done(results); - } + let filesToRead = files; + if (token.count + files.length > MAX_FILES) { + token.maxReached = true; + pending = MAX_FILES - token.count; + filesToRead = files.slice(0, pending); + } + + token.count += files.length; + for (const file of filesToRead) { stat(join(dir, file), (err, stats) => { // Ignore files that can't be read if (err) { @@ -96,11 +119,6 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise } } } else { - if (token.count >= MAX_FILES) { - token.maxReached = true; - } - - token.count++; results.push(file); if (--pending === 0) { @@ -514,7 +532,7 @@ export class DiagnosticsService implements IDiagnosticsService { } } - public async reportWorkspaceStats(workspace: IWorkspace): Promise { + public async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise { workspace.folders.forEach(folder => { const folderUri = URI.revive(folder.uri); if (folderUri.scheme === 'file') { @@ -525,16 +543,19 @@ export class DiagnosticsService implements IDiagnosticsService { count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; type WorkspaceStatsClassification = { + 'workspace.id': { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; fileTypes: WorkspaceStatItemClassification; configTypes: WorkspaceStatItemClassification; launchConfigs: WorkspaceStatItemClassification; }; type WorkspaceStatsEvent = { + 'workspace.id': string | undefined; fileTypes: WorkspaceStatItem[]; configTypes: WorkspaceStatItem[]; launchConfigs: WorkspaceStatItem[]; }; this.telemetryService.publicLog2('workspace.stats', { + 'workspace.id': workspace.telemetryId, fileTypes: stats.fileTypes, configTypes: stats.configFiles, launchConfigs: stats.launchConfigFiles @@ -545,4 +566,4 @@ export class DiagnosticsService implements IDiagnosticsService { } }); } -} \ No newline at end of file +} diff --git a/src/vs/platform/download/common/downloadService.ts b/src/vs/platform/download/common/downloadService.ts index a2b8365df4c91..54d2f1a98357d 100644 --- a/src/vs/platform/download/common/downloadService.ts +++ b/src/vs/platform/download/common/downloadService.ts @@ -20,7 +20,7 @@ export class DownloadService implements IDownloadService { ) { } async download(resource: URI, target: URI, cancellationToken: CancellationToken = CancellationToken.None): Promise { - if (resource.scheme === Schemas.file) { + if (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote) { await this.fileService.copy(resource, target); return; } diff --git a/src/vs/platform/driver/browser/baseDriver.ts b/src/vs/platform/driver/browser/baseDriver.ts new file mode 100644 index 0000000000000..38b5626a500dc --- /dev/null +++ b/src/vs/platform/driver/browser/baseDriver.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom'; +import { coalesce } from 'vs/base/common/arrays'; +import { IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; + +function serializeElement(element: Element, recursive: boolean): IElement { + const attributes = Object.create(null); + + for (let j = 0; j < element.attributes.length; j++) { + const attr = element.attributes.item(j); + if (attr) { + attributes[attr.name] = attr.value; + } + } + + const children: IElement[] = []; + + if (recursive) { + for (let i = 0; i < element.children.length; i++) { + const child = element.children.item(i); + if (child) { + children.push(serializeElement(child, true)); + } + } + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + + return { + tagName: element.tagName, + className: element.className, + textContent: element.textContent || '', + attributes, + children, + left, + top + }; +} + +export abstract class BaseWindowDriver implements IWindowDriver { + + constructor() { } + + abstract click(selector: string, xoffset?: number, yoffset?: number): Promise; + abstract doubleClick(selector: string): Promise; + + async setValue(selector: string, text: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + return Promise.reject(new Error(`Element not found: ${selector}`)); + } + + const inputElement = element as HTMLInputElement; + inputElement.value = text; + + const event = new Event('input', { bubbles: true, cancelable: true }); + inputElement.dispatchEvent(event); + } + + async getTitle(): Promise { + return document.title; + } + + async isActiveElement(selector: string): Promise { + const element = document.querySelector(selector); + + if (element !== document.activeElement) { + const chain: string[] = []; + let el = document.activeElement; + + while (el) { + const tagName = el.tagName; + const id = el.id ? `#${el.id}` : ''; + const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join(''); + chain.unshift(`${tagName}${id}${classes}`); + + el = el.parentElement; + } + + throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`); + } + + return true; + } + + async getElements(selector: string, recursive: boolean): Promise { + const query = document.querySelectorAll(selector); + const result: IElement[] = []; + + for (let i = 0; i < query.length; i++) { + const element = query.item(i); + result.push(serializeElement(element, recursive)); + } + + return result; + } + + async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }> { + const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; + return this._getElementXY(selector, offset); + } + + async typeInEditor(selector: string, text: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Editor not found: ${selector}`); + } + + const textarea = element as HTMLTextAreaElement; + const start = textarea.selectionStart; + const newStart = start + text.length; + const value = textarea.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + textarea.value = newValue; + textarea.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + textarea.dispatchEvent(event); + } + + async getTerminalBuffer(selector: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Terminal not found: ${selector}`); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error(`Xterm not found: ${selector}`); + } + + const lines: string[] = []; + + for (let i = 0; i < xterm.buffer.length; i++) { + lines.push(xterm.buffer.getLine(i)!.translateToString(true)); + } + + return lines; + } + + async writeInTerminal(selector: string, text: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Element not found: ${selector}`); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error(`Xterm not found: ${selector}`); + } + + xterm._core._coreService.triggerDataEvent(text); + } + + protected async _getElementXY(selector: string, offset?: { x: number, y: number }): Promise<{ x: number; y: number; }> { + const element = document.querySelector(selector); + + if (!element) { + return Promise.reject(new Error(`Element not found: ${selector}`)); + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + const { width, height } = getClientArea(element as HTMLElement); + let x: number, y: number; + + if (offset) { + x = left + offset.x; + y = top + offset.y; + } else { + x = left + (width / 2); + y = top + (height / 2); + } + + x = Math.round(x); + y = Math.round(y); + + return { x, y }; + } + + abstract async openDevTools(): Promise; +} diff --git a/src/vs/platform/driver/browser/driver.ts b/src/vs/platform/driver/browser/driver.ts new file mode 100644 index 0000000000000..f13d45a5f13d7 --- /dev/null +++ b/src/vs/platform/driver/browser/driver.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; + +class BrowserWindowDriver extends BaseWindowDriver { + click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise { + throw new Error('Method not implemented.'); + } + doubleClick(selector: string): Promise { + throw new Error('Method not implemented.'); + } + openDevTools(): Promise { + throw new Error('Method not implemented.'); + } +} + +export async function registerWindowDriver(): Promise { + (window).driver = new BrowserWindowDriver(); + + return toDisposable(() => { + return { dispose: () => { } }; + }); +} diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts new file mode 100644 index 0000000000000..29fde87599441 --- /dev/null +++ b/src/vs/platform/driver/common/driver.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +// !! Do not remove the following START and END markers, they are parsed by the smoketest build + +//*START +export interface IElement { + tagName: string; + className: string; + textContent: string; + attributes: { [name: string]: string; }; + children: IElement[]; + top: number; + left: number; +} + +export interface IDriver { + _serviceBrand: any; + + getWindowIds(): Promise; + capturePage(windowId: number): Promise; + reloadWindow(windowId: number): Promise; + exitApplication(): Promise; + dispatchKeybinding(windowId: number, keybinding: string): Promise; + click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; + doubleClick(windowId: number, selector: string): Promise; + setValue(windowId: number, selector: string, text: string): Promise; + getTitle(windowId: number): Promise; + isActiveElement(windowId: number, selector: string): Promise; + getElements(windowId: number, selector: string, recursive?: boolean): Promise; + getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>; + typeInEditor(windowId: number, selector: string, text: string): Promise; + getTerminalBuffer(windowId: number, selector: string): Promise; + writeInTerminal(windowId: number, selector: string, text: string): Promise; +} +//*END + +export const ID = 'driverService'; +export const IDriver = createDecorator(ID); + +export interface IWindowDriver { + click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; + doubleClick(selector: string): Promise; + setValue(selector: string, text: string): Promise; + getTitle(): Promise; + isActiveElement(selector: string): Promise; + getElements(selector: string, recursive: boolean): Promise; + getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>; + typeInEditor(selector: string, text: string): Promise; + getTerminalBuffer(selector: string): Promise; + writeInTerminal(selector: string, text: string): Promise; +} diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts index a956739bed7f1..09e2d6b3379db 100644 --- a/src/vs/platform/driver/electron-browser/driver.ts +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -4,55 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IWindowDriver, IElement, WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver'; +import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; -import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom'; import * as electron from 'electron'; import { IWindowService } from 'vs/platform/windows/common/windows'; -import { Terminal } from 'xterm'; import { timeout } from 'vs/base/common/async'; -import { coalesce } from 'vs/base/common/arrays'; +import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; -function serializeElement(element: Element, recursive: boolean): IElement { - const attributes = Object.create(null); - - for (let j = 0; j < element.attributes.length; j++) { - const attr = element.attributes.item(j); - if (attr) { - attributes[attr.name] = attr.value; - } - } - - const children: IElement[] = []; - - if (recursive) { - for (let i = 0; i < element.children.length; i++) { - const child = element.children.item(i); - if (child) { - children.push(serializeElement(child, true)); - } - } - } - - const { left, top } = getTopLeftOffset(element as HTMLElement); - - return { - tagName: element.tagName, - className: element.className, - textContent: element.textContent || '', - attributes, - children, - left, - top - }; -} - -class WindowDriver implements IWindowDriver { +class WindowDriver extends BaseWindowDriver { constructor( @IWindowService private readonly windowService: IWindowService - ) { } + ) { + super(); + } click(selector: string, xoffset?: number, yoffset?: number): Promise { const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; @@ -63,31 +29,6 @@ class WindowDriver implements IWindowDriver { return this._click(selector, 2); } - private async _getElementXY(selector: string, offset?: { x: number, y: number }): Promise<{ x: number; y: number; }> { - const element = document.querySelector(selector); - - if (!element) { - return Promise.reject(new Error(`Element not found: ${selector}`)); - } - - const { left, top } = getTopLeftOffset(element as HTMLElement); - const { width, height } = getClientArea(element as HTMLElement); - let x: number, y: number; - - if (offset) { - x = left + offset.x; - y = top + offset.y; - } else { - x = left + (width / 2); - y = top + (height / 2); - } - - x = Math.round(x); - y = Math.round(y); - - return { x, y }; - } - private async _click(selector: string, clickCount: number, offset?: { x: number, y: number }): Promise { const { x, y } = await this._getElementXY(selector, offset); @@ -99,116 +40,6 @@ class WindowDriver implements IWindowDriver { await timeout(100); } - async setValue(selector: string, text: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - return Promise.reject(new Error(`Element not found: ${selector}`)); - } - - const inputElement = element as HTMLInputElement; - inputElement.value = text; - - const event = new Event('input', { bubbles: true, cancelable: true }); - inputElement.dispatchEvent(event); - } - - async getTitle(): Promise { - return document.title; - } - - async isActiveElement(selector: string): Promise { - const element = document.querySelector(selector); - - if (element !== document.activeElement) { - const chain: string[] = []; - let el = document.activeElement; - - while (el) { - const tagName = el.tagName; - const id = el.id ? `#${el.id}` : ''; - const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join(''); - chain.unshift(`${tagName}${id}${classes}`); - - el = el.parentElement; - } - - throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`); - } - - return true; - } - - async getElements(selector: string, recursive: boolean): Promise { - const query = document.querySelectorAll(selector); - const result: IElement[] = []; - - for (let i = 0; i < query.length; i++) { - const element = query.item(i); - result.push(serializeElement(element, recursive)); - } - - return result; - } - - async typeInEditor(selector: string, text: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Editor not found: ${selector}`); - } - - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); - } - - async getTerminalBuffer(selector: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Terminal not found: ${selector}`); - } - - const xterm: Terminal = (element as any).xterm; - - if (!xterm) { - throw new Error(`Xterm not found: ${selector}`); - } - - const lines: string[] = []; - - for (let i = 0; i < xterm.buffer.length; i++) { - lines.push(xterm.buffer.getLine(i)!.translateToString(true)); - } - - return lines; - } - - async writeInTerminal(selector: string, text: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Element not found: ${selector}`); - } - - const xterm: Terminal = (element as any).xterm; - - if (!xterm) { - throw new Error(`Xterm not found: ${selector}`); - } - - xterm._core._coreService.triggerDataEvent(text); - } - async openDevTools(): Promise { await this.windowService.openDevTools({ mode: 'detach' }); } diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 2b341fb2ed6f7..8096b8f6abda7 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDriver, DriverChannel, IElement, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver, IDriverOptions } from 'vs/platform/driver/node/driver'; +import { DriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IDriverOptions } from 'vs/platform/driver/node/driver'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -17,6 +17,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { ScanCodeBinding } from 'vs/base/common/scanCode'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { timeout } from 'vs/base/common/async'; +import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; function isSilentKeyCode(keyCode: KeyCode) { return keyCode < KeyCode.KEY_0; @@ -163,6 +164,11 @@ export class Driver implements IDriver, IWindowDriverRegistry { return await windowDriver.getElements(selector, recursive); } + async getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }> { + const windowDriver = await this.getWindowDriver(windowId); + return await windowDriver.getElementXY(selector, xoffset, yoffset); + } + async typeInEditor(windowId: number, selector: string, text: string): Promise { const windowDriver = await this.getWindowDriver(windowId); await windowDriver.typeInEditor(selector, text); diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index e77790a51832d..2c525c0e69421 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -5,45 +5,9 @@ import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; - -export const ID = 'driverService'; -export const IDriver = createDecorator(ID); - -// !! Do not remove the following START and END markers, they are parsed by the smoketest build - -//*START -export interface IElement { - tagName: string; - className: string; - textContent: string; - attributes: { [name: string]: string; }; - children: IElement[]; - top: number; - left: number; -} - -export interface IDriver { - _serviceBrand: any; - - getWindowIds(): Promise; - capturePage(windowId: number): Promise; - reloadWindow(windowId: number): Promise; - exitApplication(): Promise; - dispatchKeybinding(windowId: number, keybinding: string): Promise; - click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - doubleClick(windowId: number, selector: string): Promise; - setValue(windowId: number, selector: string, text: string): Promise; - getTitle(windowId: number): Promise; - isActiveElement(windowId: number, selector: string): Promise; - getElements(windowId: number, selector: string, recursive?: boolean): Promise; - typeInEditor(windowId: number, selector: string, text: string): Promise; - getTerminalBuffer(windowId: number, selector: string): Promise; - writeInTerminal(windowId: number, selector: string, text: string): Promise; -} -//*END +import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; export class DriverChannel implements IServerChannel { @@ -66,6 +30,7 @@ export class DriverChannel implements IServerChannel { case 'getTitle': return this.driver.getTitle(arg[0]); case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); + case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]); @@ -125,6 +90,10 @@ export class DriverChannelClient implements IDriver { return this.channel.call('getElements', [windowId, selector, recursive]); } + getElementXY(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<{ x: number, y: number }> { + return this.channel.call('getElementXY', [windowId, selector, xoffset, yoffset]); + } + typeInEditor(windowId: number, selector: string, text: string): Promise { return this.channel.call('typeInEditor', [windowId, selector, text]); } @@ -180,18 +149,6 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry } } -export interface IWindowDriver { - click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - doubleClick(selector: string): Promise; - setValue(selector: string, text: string): Promise; - getTitle(): Promise; - isActiveElement(selector: string): Promise; - getElements(selector: string, recursive: boolean): Promise; - typeInEditor(selector: string, text: string): Promise; - getTerminalBuffer(selector: string): Promise; - writeInTerminal(selector: string, text: string): Promise; -} - export class WindowDriverChannel implements IServerChannel { constructor(private driver: IWindowDriver) { } @@ -208,6 +165,7 @@ export class WindowDriverChannel implements IServerChannel { case 'getTitle': return this.driver.getTitle(); case 'isActiveElement': return this.driver.isActiveElement(arg); case 'getElements': return this.driver.getElements(arg[0], arg[1]); + case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]); @@ -247,6 +205,10 @@ export class WindowDriverChannelClient implements IWindowDriver { return this.channel.call('getElements', [selector, recursive]); } + getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number, y: number }> { + return this.channel.call('getElementXY', [selector, xoffset, yoffset]); + } + typeInEditor(selector: string, text: string): Promise { return this.channel.call('typeInEditor', [selector, text]); } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index b13a82f4447ef..3b45568e1b8f4 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -140,24 +140,15 @@ export interface IEnvironmentService { extensionTestsLocationURI?: URI; debugExtensionHost: IExtensionHostDebugParams; - debugSearch: IDebugParams; - - logExtensionHostCommunication: boolean; isBuilt: boolean; wait: boolean; status: boolean; - // logging log?: string; logsPath: string; verbose: boolean; - skipGettingStarted: boolean | undefined; - skipReleaseNotes: boolean | undefined; - - skipAddToRecentlyOpened: boolean; - mainIPCHandle: string; sharedIPCHandle: string; @@ -170,9 +161,5 @@ export interface IEnvironmentService { driverHandle?: string; driverVerbose: boolean; - webviewEndpoint?: string; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; - - readonly galleryMachineIdResource?: URI; + galleryMachineIdResource?: URI; } diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index c5c4e66272c48..a07c4c8f483e6 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -236,26 +236,15 @@ export class EnvironmentService implements IEnvironmentService { return false; } - get skipGettingStarted(): boolean { return !!this._args['skip-getting-started']; } - - get skipReleaseNotes(): boolean { return !!this._args['skip-release-notes']; } - - get skipAddToRecentlyOpened(): boolean { return !!this._args['skip-add-to-recently-opened']; } - @memoize get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this._args, this.isBuilt); } - @memoize - get debugSearch(): IDebugParams { return parseSearchPort(this._args, this.isBuilt); } - get isBuilt(): boolean { return !process.env['VSCODE_DEV']; } get verbose(): boolean { return !!this._args.verbose; } get log(): string | undefined { return this._args.log; } get wait(): boolean { return !!this._args.wait; } - get logExtensionHostCommunication(): boolean { return !!this._args.logExtensionHostCommunication; } - get status(): boolean { return !!this._args.status; } @memoize @@ -276,9 +265,6 @@ export class EnvironmentService implements IEnvironmentService { get driverHandle(): string | undefined { return this._args['driver']; } get driverVerbose(): boolean { return !!this._args['driver-verbose']; } - readonly webviewResourceRoot = 'vscode-resource:{{resource}}'; - readonly webviewCspSource = 'vscode-resource:'; - constructor(private _args: ParsedArgs, private _execPath: string) { if (!process.env['VSCODE_LOGS']) { const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''); diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e291d4361155d..e33d7a5c55d5a 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -9,7 +9,8 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery import { assign, getOrDefault } from 'vs/base/common/objects'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPager } from 'vs/base/common/paging'; -import { IRequestService, IRequestOptions, IRequestContext, asJson, asText, IHeaders } from 'vs/platform/request/common/request'; +import { IRequestService, asJson, asText } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { generateUuid, isUUID } from 'vs/base/common/uuid'; @@ -817,4 +818,4 @@ export async function resolveMarketplaceHeaders(version: string, environmentServ return headers; -} \ No newline at end of file +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index b075553498dc7..dd19996782dfe 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -195,6 +195,7 @@ export interface IExtensionManagementService { zip(extension: ILocalExtension): Promise; unzip(zipLocation: URI, type: ExtensionType): Promise; + getManifest(vsix: URI): Promise; install(vsix: URI): Promise; installFromGallery(extension: IGalleryExtension): Promise; uninstall(extension: ILocalExtension, force?: boolean): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index b88350f44c714..349223b9210d1 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { cloneAndChange } from 'vs/base/common/objects'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI { return URI.revive(transformer ? transformer.transformIncoming(uri) : uri); @@ -62,6 +62,7 @@ export class ExtensionManagementChannel implements IServerChannel { case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer)); case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer), args[1]); case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); + case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); case 'installFromGallery': return this.service.installFromGallery(args[0]); case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]); case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); @@ -99,6 +100,10 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer return Promise.resolve(this.channel.call('install', [vsix])).then(local => transformIncomingExtension(local, null)); } + getManifest(vsix: URI): Promise { + return Promise.resolve(this.channel.call('getManifest', [vsix])); + } + installFromGallery(extension: IGalleryExtension): Promise { return Promise.resolve(this.channel.call('installFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 70cd46c824a20..7d0b568fc7f75 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -161,6 +161,12 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.install(zipLocation, type).then(local => local.identifier); } + async getManifest(vsix: URI): Promise { + const downloadLocation = await this.downloadVsix(vsix); + const zipPath = path.resolve(downloadLocation.fsPath); + return getManifest(zipPath); + } + private collectFiles(extension: ILocalExtension): Promise { const collectFilesFromDirectory = async (dir: string): Promise => { @@ -983,4 +989,4 @@ export class ExtensionManagementService extends Disposable implements IExtension */ this.telemetryService.publicLog(eventName, assign(extensionData, { success: !error, duration, errorcode })); } -} \ No newline at end of file +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 93a52746e6bba..a043fdfcaa3fa 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -106,7 +106,7 @@ export interface IExtensionContributions { localizations?: ILocalization[]; } -export type ExtensionKind = 'ui' | 'workspace'; +export type ExtensionKind = 'ui' | 'workspace' | 'web'; export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing @@ -221,4 +221,4 @@ export interface IExtensionDescription extends IExtensionManifest { export function isLanguagePackExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false; -} \ No newline at end of file +} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index ad600a2a430fd..40d21fce9de44 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -620,26 +620,26 @@ export interface IReadFileOptions { * that have been read already with the same etag. * It is the task of the caller to makes sure to handle this error case from the promise. */ - etag?: string; + readonly etag?: string; /** * Is an integer specifying where to begin reading from in the file. If position is null, * data will be read from the current file position. */ - position?: number; + readonly position?: number; /** * Is an integer specifying how many bytes to read from the file. By default, all bytes * will be read. */ - length?: number; + readonly length?: number; /** * If provided, the size of the file will be checked against the limits. */ limits?: { - size?: number; - memory?: number; + readonly size?: number; + readonly memory?: number; }; } @@ -648,12 +648,12 @@ export interface IWriteFileOptions { /** * The last known modification time of the file. This can be used to prevent dirty writes. */ - mtime?: number; + readonly mtime?: number; /** * The etag of the file. This can be used to prevent dirty writes. */ - etag?: string; + readonly etag?: string; } export interface IResolveFileOptions { @@ -662,22 +662,22 @@ export interface IResolveFileOptions { * Automatically continue resolving children of a directory until the provided resources * are found. */ - resolveTo?: URI[]; + readonly resolveTo?: readonly URI[]; /** * Automatically continue resolving children of a directory if the number of children is 1. */ - resolveSingleChildDescendants?: boolean; + readonly resolveSingleChildDescendants?: boolean; /** * Will resolve mtime, size and etag of files if enabled. This can have a negative impact * on performance and thus should only be used when these values are required. */ - resolveMetadata?: boolean; + readonly resolveMetadata?: boolean; } export interface IResolveMetadataFileOptions extends IResolveFileOptions { - resolveMetadata: true; + readonly resolveMetadata: true; } export interface ICreateFileOptions { @@ -686,7 +686,7 @@ export interface ICreateFileOptions { * Overwrite the file to create if it already exists on disk. Otherwise * an error will be thrown (FILE_MODIFIED_SINCE). */ - overwrite?: boolean; + readonly overwrite?: boolean; } export class FileOperationError extends Error { diff --git a/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts b/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts index c2d96732a3c4f..1948481a857d3 100644 --- a/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts +++ b/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts @@ -134,7 +134,7 @@ export class OutOfProcessWin32FolderWatcher { public dispose(): void { if (this.handle) { this.handle.kill(); - this.handle = null!; // StrictNullOverride: nulling out ok in dispose + this.handle = undefined; } } } diff --git a/src/vs/platform/history/common/historyStorage.ts b/src/vs/platform/history/common/historyStorage.ts index 089d73ec3139f..b026460963d66 100644 --- a/src/vs/platform/history/common/historyStorage.ts +++ b/src/vs/platform/history/common/historyStorage.ts @@ -71,7 +71,7 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine result.workspaces.push({ folderUri: URI.file(workspace) }); } else if (isLegacySerializedWorkspace(workspace)) { result.workspaces.push({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } }); - } else if (isUriComponents(window)) { + } else if (isUriComponents(workspace)) { // added by 1.26-insiders result.workspaces.push({ folderUri: URI.revive(workspace) }); } diff --git a/src/vs/platform/issue/electron-browser/issueService.ts b/src/vs/platform/issue/electron-browser/issueService.ts index 29ea55b6044a9..6228ef3367121 100644 --- a/src/vs/platform/issue/electron-browser/issueService.ts +++ b/src/vs/platform/issue/electron-browser/issueService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IIssueService, IssueReporterData, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import { IIssueService, IssueReporterData, ProcessExplorerData } from 'vs/platform/issue/node/issue'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index 48b0293e933a6..aa31eb1913aa1 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -6,10 +6,11 @@ import { localize } from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/node/issue'; import { BrowserWindow, ipcMain, screen, Event, dialog } from 'electron'; import { ILaunchService } from 'vs/platform/launch/electron-main/launchService'; -import { PerformanceInfo, IDiagnosticsService, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; +import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/node/issue.ts similarity index 100% rename from src/vs/platform/issue/common/issue.ts rename to src/vs/platform/issue/node/issue.ts diff --git a/src/vs/platform/issue/node/issueIpc.ts b/src/vs/platform/issue/node/issueIpc.ts index 4eca97ef53cb1..271bcf5ceeebb 100644 --- a/src/vs/platform/issue/node/issueIpc.ts +++ b/src/vs/platform/issue/node/issueIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IIssueService } from 'vs/platform/issue/common/issue'; +import { IIssueService } from 'vs/platform/issue/node/issue'; export class IssueChannel implements IServerChannel { diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index ffc20e10abb85..4565228e69032 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -21,6 +21,7 @@ export interface ILabelService { * If noPrefix is passed does not tildify the label and also does not prepand the root name for relative labels in a multi root scenario. */ getUriLabel(resource: URI, options?: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean }): string; + getUriBasenameLabel(resource: URI): string; getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(scheme: string, authority?: string): string; getSeparator(scheme: string, authority?: string): '/' | '\\'; diff --git a/src/vs/platform/launch/electron-main/launchService.ts b/src/vs/platform/launch/electron-main/launchService.ts index 95e56eae4dacb..c9924cf39d531 100644 --- a/src/vs/platform/launch/electron-main/launchService.ts +++ b/src/vs/platform/launch/electron-main/launchService.ts @@ -19,7 +19,7 @@ import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron'; import { Event } from 'vs/base/common/event'; import { hasArgs } from 'vs/platform/environment/node/argv'; import { coalesce } from 'vs/base/common/arrays'; -import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/common/launchService'; export const ID = 'launchService'; diff --git a/src/vs/platform/lifecycle/browser/lifecycleService.ts b/src/vs/platform/lifecycle/browser/lifecycleService.ts index 9a85a58262ec3..bd34fc59e8ffc 100644 --- a/src/vs/platform/lifecycle/browser/lifecycleService.ts +++ b/src/vs/platform/lifecycle/browser/lifecycleService.ts @@ -22,10 +22,12 @@ export class BrowserLifecycleService extends AbstractLifecycleService { } private registerListeners(): void { - window.onbeforeunload = () => this.beforeUnload(); + // Note: we cannot change this to window.addEventListener('beforeUnload') + // because it seems that mechanism does not allow for preventing the unload + window.onbeforeunload = () => this.onBeforeUnload(); } - private beforeUnload(): string | null { + private onBeforeUnload(): string | null { let veto = false; // Before Shutdown @@ -34,7 +36,7 @@ export class BrowserLifecycleService extends AbstractLifecycleService { if (value === true) { veto = true; } else if (value instanceof Promise && !veto) { - console.warn(new Error('Long running onBeforeShutdown currently not supported')); + console.warn(new Error('Long running onBeforeShutdown currently not supported in the web')); veto = true; } }, @@ -49,11 +51,14 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // No Veto: continue with Will Shutdown this._onWillShutdown.fire({ join() { - console.warn(new Error('Long running onWillShutdown currently not supported')); + console.warn(new Error('Long running onWillShutdown currently not supported in the web')); }, reason: ShutdownReason.QUIT }); + // Finally end with Shutdown event + this._onShutdown.fire(); + return null; } } diff --git a/src/vs/platform/log/common/fileLogService.ts b/src/vs/platform/log/common/fileLogService.ts new file mode 100644 index 0000000000000..01859d4f7b24b --- /dev/null +++ b/src/vs/platform/log/common/fileLogService.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService, LogLevel, AbstractLogService } from 'vs/platform/log/common/log'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Queue } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; + +export class FileLogService extends AbstractLogService implements ILogService { + + _serviceBrand: any; + + private readonly queue: Queue; + + constructor( + private readonly name: string, + private readonly resource: URI, + level: LogLevel, + @IFileService private readonly fileService: IFileService + ) { + super(); + this.setLevel(level); + this.queue = this._register(new Queue()); + } + + trace(): void { + if (this.getLevel() <= LogLevel.Trace) { + this._log(LogLevel.Trace, this.format(arguments)); + } + } + + debug(): void { + if (this.getLevel() <= LogLevel.Debug) { + this._log(LogLevel.Debug, this.format(arguments)); + } + } + + info(): void { + if (this.getLevel() <= LogLevel.Info) { + this._log(LogLevel.Info, this.format(arguments)); + } + } + + warn(): void { + if (this.getLevel() <= LogLevel.Warning) { + this._log(LogLevel.Warning, this.format(arguments)); + } + } + + error(): void { + if (this.getLevel() <= LogLevel.Error) { + const arg = arguments[0]; + + if (arg instanceof Error) { + const array = Array.prototype.slice.call(arguments) as any[]; + array[0] = arg.stack; + this._log(LogLevel.Error, this.format(array)); + } else { + this._log(LogLevel.Error, this.format(arguments)); + } + } + } + + critical(): void { + if (this.getLevel() <= LogLevel.Critical) { + this._log(LogLevel.Critical, this.format(arguments)); + } + } + + flush(): Promise { + return this.queue.queue(() => Promise.resolve()); + } + + log(level: LogLevel, args: any[]): void { + this._log(level, this.format(args)); + } + + private _log(level: LogLevel, message: string): void { + this.queue.queue(async () => { + let content = await this.loadContent(); + content += `[${this.getCurrentTimestamp()}] [${this.name}] [${this.stringifyLogLevel(level)}] ${message}\n`; + await this.fileService.writeFile(this.resource, VSBuffer.fromString(content)); + }); + } + + private getCurrentTimestamp(): string { + const toTwoDigits = (v: number) => v < 10 ? `0${v}` : v; + const toThreeDigits = (v: number) => v < 10 ? `00${v}` : v < 100 ? `0${v}` : v; + const currentTime = new Date(); + return `${currentTime.getFullYear()}-${toTwoDigits(currentTime.getMonth() + 1)}-${toTwoDigits(currentTime.getDate())} ${toTwoDigits(currentTime.getHours())}:${toTwoDigits(currentTime.getMinutes())}:${toTwoDigits(currentTime.getSeconds())}.${toThreeDigits(currentTime.getMilliseconds())}`; + } + + private async loadContent(): Promise { + try { + const content = await this.fileService.readFile(this.resource); + return content.value.toString(); + } catch (e) { + return ''; + } + } + + private stringifyLogLevel(level: LogLevel): string { + switch (level) { + case LogLevel.Critical: return 'critical'; + case LogLevel.Debug: return 'debug'; + case LogLevel.Error: return 'error'; + case LogLevel.Info: return 'info'; + case LogLevel.Trace: return 'trace'; + case LogLevel.Warning: return 'warning'; + } + return ''; + } + + private format(args: any): string { + let result = ''; + + for (let i = 0; i < args.length; i++) { + let a = args[i]; + + if (typeof a === 'object') { + try { + a = JSON.stringify(a); + } catch (e) { } + } + + result += (i > 0 ? ' ' : '') + a; + } + + return result; + } +} diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 235c4b12f3640..26d05bc8c2e49 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -129,6 +129,10 @@ export interface MarkerStatistics { export namespace IMarkerData { const emptyString = ''; export function makeKey(markerData: IMarkerData): string { + return makeKeyOptionalMessage(markerData, true); + } + + export function makeKeyOptionalMessage(markerData: IMarkerData, useMessage: boolean): string { let result: string[] = [emptyString]; if (markerData.source) { result.push(markerData.source.replace('¦', '\¦')); @@ -145,7 +149,10 @@ export namespace IMarkerData { } else { result.push(emptyString); } - if (markerData.message) { + + // Modifed to not include the message as part of the marker key to work around + // https://github.com/microsoft/vscode/issues/77475 + if (markerData.message && useMessage) { result.push(markerData.message.replace('¦', '\¦')); } else { result.push(emptyString); diff --git a/src/vs/platform/menubar/electron-browser/menubarService.ts b/src/vs/platform/menubar/electron-browser/menubarService.ts index 794d65de6a7fd..336330d1e6d7c 100644 --- a/src/vs/platform/menubar/electron-browser/menubarService.ts +++ b/src/vs/platform/menubar/electron-browser/menubarService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; +import { IMenubarService, IMenubarData } from 'vs/platform/menubar/node/menubar'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 01f781dd94490..ff9d06a636b69 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { mnemonicMenuLabel as baseMnemonicLabel } from 'vs/base/common/labels'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; -import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; +import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/node/menubar'; import { URI } from 'vs/base/common/uri'; import { IStateService } from 'vs/platform/state/common/state'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; diff --git a/src/vs/platform/menubar/electron-main/menubarService.ts b/src/vs/platform/menubar/electron-main/menubarService.ts index b4a8ed44ef489..f7abf2ee9f26d 100644 --- a/src/vs/platform/menubar/electron-main/menubarService.ts +++ b/src/vs/platform/menubar/electron-main/menubarService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; +import { IMenubarService, IMenubarData } from 'vs/platform/menubar/node/menubar'; import { Menubar } from 'vs/platform/menubar/electron-main/menubar'; import { ILogService } from 'vs/platform/log/common/log'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/menubar/common/menubar.ts b/src/vs/platform/menubar/node/menubar.ts similarity index 100% rename from src/vs/platform/menubar/common/menubar.ts rename to src/vs/platform/menubar/node/menubar.ts diff --git a/src/vs/platform/menubar/node/menubarIpc.ts b/src/vs/platform/menubar/node/menubarIpc.ts index 58f22e631b472..850a2542dd05d 100644 --- a/src/vs/platform/menubar/node/menubarIpc.ts +++ b/src/vs/platform/menubar/node/menubarIpc.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { IMenubarService } from 'vs/platform/menubar/node/menubar'; import { Event } from 'vs/base/common/event'; export class MenubarChannel implements IServerChannel { diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 2582047934eb3..23b14135edbca 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -37,6 +37,19 @@ export interface INotificationProperties { neverShowAgain?: INeverShowAgainOptions; } +export enum NeverShowAgainScope { + + /** + * Will never show this notification on the current workspace again. + */ + WORKSPACE, + + /** + * Will never show this notification on any workspace again. + */ + GLOBAL +} + export interface INeverShowAgainOptions { /** @@ -49,6 +62,12 @@ export interface INeverShowAgainOptions { * make it a secondary action instead. */ isSecondary?: boolean; + + /** + * Wether to persist the choice in the current workspace or for all workspaces. By + * default it will be persisted for all workspaces. + */ + scope?: NeverShowAgainScope; } export interface INotification extends INotificationProperties { diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 8c0f9fee5df42..8c92cb2d1bdd2 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -9,17 +9,30 @@ import { IDisposable } from 'vs/base/common/lifecycle'; export const IOpenerService = createDecorator('openerService'); - export interface IOpener { open(resource: URI, options?: { openToSide?: boolean }): Promise; + open(resource: URI, options?: { openExternal?: boolean }): Promise; +} + +export interface IValidator { + shouldOpen(resource: URI): Promise; } export interface IOpenerService { _serviceBrand: any; + /** + * Register a participant that can handle the open() call. + */ registerOpener(opener: IOpener): IDisposable; + /** + * Register a participant that can validate if the URI resource be opened. + * validators are run before openers. + */ + registerValidator(validator: IValidator): IDisposable; + /** * Opens a resource, like a webaddress, a document uri, or executes command. * @@ -27,10 +40,12 @@ export interface IOpenerService { * @return A promise that resolves when the opening is done. */ open(resource: URI, options?: { openToSide?: boolean }): Promise; + open(resource: URI, options?: { openExternal?: boolean }): Promise; } export const NullOpenerService: IOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return { dispose() { } }; }, - open() { return Promise.resolve(false); } + registerValidator() { return { dispose() { } }; }, + open() { return Promise.resolve(false); }, }); diff --git a/src/vs/platform/product/browser/productService.ts b/src/vs/platform/product/browser/productService.ts deleted file mode 100644 index 70bc0b31bcc14..0000000000000 --- a/src/vs/platform/product/browser/productService.ts +++ /dev/null @@ -1,49 +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 { IProductService, IProductConfiguration } from 'vs/platform/product/common/product'; -import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; - -export class ProductService implements IProductService { - - _serviceBrand!: ServiceIdentifier; - - private readonly productConfiguration: IProductConfiguration | null; - - constructor() { - const element = document.getElementById('vscode-remote-product-configuration'); - this.productConfiguration = element ? JSON.parse(element.getAttribute('data-settings')!) : null; - } - - get version(): string { return this.productConfiguration && this.productConfiguration.version ? this.productConfiguration.version : '1.38.0-unknown'; } - - get commit(): string | undefined { return this.productConfiguration ? this.productConfiguration.commit : undefined; } - - get nameLong(): string { return this.productConfiguration ? this.productConfiguration.nameLong : 'Unknown'; } - - get urlProtocol(): string { return ''; } - - get extensionAllowedProposedApi(): readonly string[] { return this.productConfiguration ? this.productConfiguration.extensionAllowedProposedApi : []; } - - get uiExtensions(): readonly string[] | undefined { return this.productConfiguration ? this.productConfiguration.uiExtensions : undefined; } - - get enableTelemetry(): boolean { return false; } - - get sendASmile(): { reportIssueUrl: string, requestFeatureUrl: string } | undefined { return this.productConfiguration ? this.productConfiguration.sendASmile : undefined; } - - get extensionsGallery() { return this.productConfiguration ? this.productConfiguration.extensionsGallery : undefined; } - - get settingsSearchBuildId(): number | undefined { return this.productConfiguration ? this.productConfiguration.settingsSearchBuildId : undefined; } - - get settingsSearchUrl(): string | undefined { return this.productConfiguration ? this.productConfiguration.settingsSearchUrl : undefined; } - - get experimentsUrl(): string | undefined { return this.productConfiguration ? this.productConfiguration.experimentsUrl : undefined; } - - get extensionKeywords(): { [extension: string]: readonly string[]; } | undefined { return this.productConfiguration ? this.productConfiguration.extensionKeywords : undefined; } - - get extensionAllowedBadgeProviders(): readonly string[] | undefined { return this.productConfiguration ? this.productConfiguration.extensionAllowedBadgeProviders : undefined; } - - get aiConfig() { return this.productConfiguration ? this.productConfiguration.aiConfig : undefined; } -} diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 05048ee0908a3..7122abcc6f5b5 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -3,50 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IProductService = createDecorator('productService'); -export interface IProductService { +export interface IProductService extends Readonly { - _serviceBrand: ServiceIdentifier; + _serviceBrand: undefined; - readonly version: string; - readonly commit?: string; - readonly date?: string; - - readonly nameLong: string; - readonly urlProtocol: string; - readonly extensionAllowedProposedApi: readonly string[]; - readonly uiExtensions?: readonly string[]; - - readonly enableTelemetry: boolean; - readonly extensionsGallery?: { - readonly serviceUrl: string; - readonly itemUrl: string; - readonly controlUrl: string; - readonly recommendationsUrl: string; - }; - - readonly sendASmile?: { - readonly reportIssueUrl: string; - readonly requestFeatureUrl: string; - }; - - readonly settingsSearchBuildId?: number; - readonly settingsSearchUrl?: string; - - readonly experimentsUrl?: string; - readonly extensionKeywords?: { [extension: string]: readonly string[]; }; - readonly extensionAllowedBadgeProviders?: readonly string[]; - - readonly aiConfig?: { - readonly asimovKey: string; - }; } export interface IProductConfiguration { - readonly version: string; + version: string; nameShort: string; nameLong: string; readonly applicationName: string; @@ -74,11 +42,10 @@ export interface IProductConfiguration { readonly controlUrl: string; readonly recommendationsUrl: string; }; - extensionTips: { [id: string]: string; }; - extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; }; + readonly extensionTips: { [id: string]: string; }; + readonly extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; }; readonly exeBasedExtensionTips: { [id: string]: IExeBasedExtensionTip; }; readonly extensionKeywords: { [extension: string]: readonly string[]; }; - readonly extensionAllowedBadgeProviders: readonly string[]; readonly extensionAllowedProposedApi: readonly string[]; readonly keymapExtensionTips: readonly string[]; readonly crashReporter: { @@ -118,7 +85,6 @@ export interface IProductConfiguration { readonly 'linux-x64': string; readonly 'darwin': string; }; - readonly logUploaderUrl: string; readonly portable?: string; readonly uiExtensions?: readonly string[]; } diff --git a/src/vs/platform/product/node/product.ts b/src/vs/platform/product/node/product.ts index 08610d71a1a2b..3d13f26c57605 100644 --- a/src/vs/platform/product/node/product.ts +++ b/src/vs/platform/product/node/product.ts @@ -6,6 +6,7 @@ import * as path from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { IProductConfiguration } from 'vs/platform/product/common/product'; +import pkg from 'vs/platform/product/node/package'; const rootPath = path.dirname(getPathFromAmdModule(require, '')); const productJsonPath = path.join(rootPath, 'product.json'); @@ -17,4 +18,6 @@ if (process.env['VSCODE_DEV']) { product.dataFolderName += '-dev'; } +product.version = pkg.version; + export default product; diff --git a/src/vs/platform/product/node/productService.ts b/src/vs/platform/product/node/productService.ts deleted file mode 100644 index ba9c84a551db5..0000000000000 --- a/src/vs/platform/product/node/productService.ts +++ /dev/null @@ -1,42 +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 { IProductService } from 'vs/platform/product/common/product'; -import product from 'vs/platform/product/node/product'; -import pkg from 'vs/platform/product/node/package'; -import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; - -export class ProductService implements IProductService { - - _serviceBrand!: ServiceIdentifier; - - get version(): string { return pkg.version; } - - get commit(): string | undefined { return product.commit; } - - get nameLong(): string { return product.nameLong; } - - get urlProtocol(): string { return product.urlProtocol; } - - get extensionAllowedProposedApi(): readonly string[] { return product.extensionAllowedProposedApi; } - - get uiExtensions(): readonly string[] | undefined { return product.uiExtensions; } - - get enableTelemetry(): boolean { return product.enableTelemetry; } - - get sendASmile(): { reportIssueUrl: string, requestFeatureUrl: string } { return product.sendASmile; } - - get extensionsGallery() { return product.extensionsGallery; } - - get settingsSearchBuildId(): number | undefined { return product.settingsSearchBuildId; } - - get settingsSearchUrl(): string | undefined { return product.settingsSearchUrl; } - - get experimentsUrl(): string | undefined { return product.experimentsUrl; } - - get extensionKeywords(): { [extension: string]: readonly string[]; } | undefined { return product.extensionKeywords; } - - get extensionAllowedBadgeProviders(): readonly string[] | undefined { return product.extensionAllowedBadgeProviders; } -} diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 4308c72ce89e2..cbb0c6068e65e 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -50,6 +50,7 @@ export interface IProgressOptions { source?: string; total?: number; cancellable?: boolean; + buttons?: string[]; } export interface IProgressNotificationOptions extends IProgressOptions { diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 5db4ac5683e0b..3f8958b723579 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorities } from 'vs/base/common/network'; export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverService { @@ -15,13 +16,14 @@ export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverS resolveAuthority(authority: string): Promise { if (authority.indexOf(':') >= 0) { const pieces = authority.split(':'); - return Promise.resolve({ - authority: { authority, host: pieces[0], port: parseInt(pieces[1], 10) } - }); + return Promise.resolve(this._createResolvedAuthority(authority, pieces[0], parseInt(pieces[1], 10))); } - return Promise.resolve({ - authority: { authority, host: authority, port: 80 } - }); + return Promise.resolve(this._createResolvedAuthority(authority, authority, 80)); + } + + private _createResolvedAuthority(authority: string, host: string, port: number): ResolverResult { + RemoteAuthorities.set(authority, host, port); + return { authority: { authority, host, port } }; } clearResolvedAuthority(authority: string): void { diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index 5653a6912b2e0..b526d149fa3af 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -10,8 +10,10 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { ISignService } from 'vs/platform/sign/common/sign'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; export const enum ConnectionType { Management = 1, @@ -19,6 +21,17 @@ export const enum ConnectionType { Tunnel = 3, } +function connectionTypeToString(connectionType: ConnectionType): string { + switch (connectionType) { + case ConnectionType.Management: + return 'Management'; + case ConnectionType.ExtensionHost: + return 'ExtensionHost'; + case ConnectionType.Tunnel: + return 'Tunnel'; + } +} + export interface AuthRequest { type: 'auth'; auth: string; @@ -57,6 +70,7 @@ interface ISimpleConnectionOptions { reconnectionProtocol: PersistentProtocol | null; socketFactory: ISocketFactory; signService: ISignService; + logService: ILogService; } export interface IConnectCallback { @@ -67,29 +81,46 @@ export interface ISocketFactory { connect(host: string, port: number, query: string, callback: IConnectCallback): void; } -async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise { - const protocol = await new Promise((c, e) => { +async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { + const logPrefix = connectLogPrefix(options, connectionType); + const { protocol, ownsProtocol } = await new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { + options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); options.socketFactory.connect( options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, (err: any, socket: ISocket) => { if (err) { + options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); + options.logService.error(err); e(err); return; } + options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); if (options.reconnectionProtocol) { options.reconnectionProtocol.beginAcceptReconnection(socket, null); - c(options.reconnectionProtocol); + c({ protocol: options.reconnectionProtocol, ownsProtocol: false }); } else { - c(new PersistentProtocol(socket, null)); + c({ protocol: new PersistentProtocol(socket, null), ownsProtocol: true }); } } ); }); - return new Promise((c, e) => { + return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { + + const errorTimeoutToken = setTimeout(() => { + const error: any = new Error('handshake timeout'); + error.code = 'ETIMEDOUT'; + error.syscall = 'connect'; + options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`); + options.logService.error(error); + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } + e(error); + }, 10000); const messageRegistration = protocol.onControlMessage(async raw => { const msg = JSON.parse(raw.toString()); @@ -98,11 +129,16 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio const error = getErrorFromMessage(msg); if (error) { + options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); + options.logService.error(error); + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } return e(error); } if (msg.type === 'sign') { - + options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); const signed = await options.signService.sign(msg.data); const connTypeRequest: ConnectionTypeRequest = { type: 'connectionType', @@ -113,17 +149,22 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio if (args) { connTypeRequest.args = args; } + options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); - c(protocol); + clearTimeout(errorTimeoutToken); + c({ protocol, ownsProtocol }); } else { - e(new Error('handshake error')); + const error = new Error('handshake error'); + options.logService.error(`${logPrefix} received unexpected control message. Error:`); + options.logService.error(error); + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } + e(error); } }); - setTimeout(() => { - e(new Error('handshake timeout')); - }, 2000); - + options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); // TODO@vs-remote: use real nonce here const authRequest: AuthRequest = { type: 'auth', @@ -137,24 +178,37 @@ interface IManagementConnectionResult { protocol: PersistentProtocol; } -async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions): Promise { - const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.Management, undefined); - return new Promise((c, e) => { +async function connectToRemoteExtensionHostAgentAndReadOneMessage(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; firstMessage: any }> { + const startTime = Date.now(); + const logPrefix = connectLogPrefix(options, connectionType); + const { protocol, ownsProtocol } = await connectToRemoteExtensionHostAgent(options, connectionType, args); + return new Promise<{ protocol: PersistentProtocol; firstMessage: any }>((c, e) => { const registration = protocol.onControlMessage(raw => { registration.dispose(); const msg = JSON.parse(raw.toString()); const error = getErrorFromMessage(msg); if (error) { + options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); + options.logService.error(error); + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } return e(error); } if (options.reconnectionProtocol) { options.reconnectionProtocol.endAcceptReconnection(); } - c({ protocol }); + options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`); + c({ protocol, firstMessage: msg }); }); }); } +async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions): Promise { + const { protocol } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.Management, undefined); + return { protocol }; +} + export interface IRemoteExtensionHostStartParams { language: string; debugId?: string; @@ -169,22 +223,9 @@ interface IExtensionHostConnectionResult { } async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise { - const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.ExtensionHost, startArguments); - return new Promise((c, e) => { - const registration = protocol.onControlMessage(raw => { - registration.dispose(); - const msg = JSON.parse(raw.toString()); - const error = getErrorFromMessage(msg); - if (error) { - return e(error); - } - const debugPort = msg && msg.debugPort; - if (options.reconnectionProtocol) { - options.reconnectionProtocol.endAcceptReconnection(); - } - c({ protocol, debugPort }); - }); - }); + const { protocol, firstMessage } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.ExtensionHost, startArguments); + const debugPort = firstMessage && firstMessage.debugPort; + return { protocol, debugPort }; } export interface ITunnelConnectionStartParams { @@ -192,7 +233,10 @@ export interface ITunnelConnectionStartParams { } async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams): Promise { - const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams); + const startTime = Date.now(); + const logPrefix = connectLogPrefix(options, ConnectionType.Tunnel); + const { protocol } = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams); + options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`); return protocol; } @@ -201,6 +245,7 @@ export interface IConnectionOptions { socketFactory: ISocketFactory; addressProvider: IAddressProvider; signService: ISignService; + logService: ILogService; } async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise { @@ -212,7 +257,8 @@ async function resolveConnectionOptions(options: IConnectionOptions, reconnectio reconnectionToken: reconnectionToken, reconnectionProtocol: reconnectionProtocol, socketFactory: options.socketFactory, - signService: options.signService + signService: options.signService, + logService: options.logService }; } @@ -226,28 +272,48 @@ export interface IAddressProvider { } export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise { - const reconnectionToken = generateUuid(); - const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); - const { protocol } = await doConnectRemoteAgentManagement(simpleOptions); - return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol); + try { + const reconnectionToken = generateUuid(); + const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); + const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), 30 * 1000 /*30s*/); + return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol); + } catch (err) { + options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); + options.logService.error(err); + PersistentConnection.triggerPermanentFailure(); + throw err; + } } export async function connectRemoteAgentExtensionHost(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise { - const reconnectionToken = generateUuid(); - const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); - const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments); - return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort); + try { + const reconnectionToken = generateUuid(); + const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); + const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), 30 * 1000 /*30s*/); + return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort); + } catch (err) { + options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); + options.logService.error(err); + PersistentConnection.triggerPermanentFailure(); + throw err; + } } export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise { const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null); - const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }); + const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), 30 * 1000 /*30s*/); return protocol; } -function sleep(seconds: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(resolve, seconds * 1000); +function sleep(seconds: number): CancelablePromise { + return createCancelablePromise(token => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, seconds * 1000); + token.onCancellationRequested(() => { + clearTimeout(timeout); + resolve(); + }); + }); }); } @@ -264,8 +330,13 @@ export class ConnectionLostEvent { export class ReconnectionWaitEvent { public readonly type = PersistentConnectionEventType.ReconnectionWait; constructor( - public readonly durationSeconds: number + public readonly durationSeconds: number, + private readonly cancellableTimer: CancelablePromise ) { } + + public skipWait(): void { + this.cancellableTimer.cancel(); + } } export class ReconnectionRunningEvent { public readonly type = PersistentConnectionEventType.ReconnectionRunning; @@ -280,6 +351,13 @@ export type PersistenConnectionEvent = ConnectionGainEvent | ConnectionLostEvent abstract class PersistentConnection extends Disposable { + public static triggerPermanentFailure(): void { + this._permanentFailure = true; + this._instances.forEach(instance => instance._gotoPermanentFailure()); + } + private static _permanentFailure: boolean = false; + private static _instances: PersistentConnection[] = []; + private readonly _onDidStateChange = this._register(new Emitter()); public readonly onDidStateChange = this._onDidStateChange.event; @@ -288,20 +366,24 @@ abstract class PersistentConnection extends Disposable { public readonly protocol: PersistentProtocol; private _isReconnecting: boolean; - private _permanentFailure: boolean; - constructor(options: IConnectionOptions, reconnectionToken: string, protocol: PersistentProtocol) { + constructor(private readonly _connectionType: ConnectionType, options: IConnectionOptions, reconnectionToken: string, protocol: PersistentProtocol) { super(); this._options = options; this.reconnectionToken = reconnectionToken; this.protocol = protocol; this._isReconnecting = false; - this._permanentFailure = false; this._onDidStateChange.fire(new ConnectionGainEvent()); this._register(protocol.onSocketClose(() => this._beginReconnecting())); this._register(protocol.onSocketTimeout(() => this._beginReconnecting())); + + PersistentConnection._instances.push(this); + + if (PersistentConnection._permanentFailure) { + this._gotoPermanentFailure(); + } } private async _beginReconnecting(): Promise { @@ -318,10 +400,12 @@ abstract class PersistentConnection extends Disposable { } private async _runReconnectingLoop(): Promise { - if (this._permanentFailure) { + if (PersistentConnection._permanentFailure) { // no more attempts! return; } + const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true); + this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`); this._onDidStateChange.fire(new ConnectionLostEvent()); const TIMES = [5, 5, 10, 10, 10, 10, 10, 30]; const disconnectStartTime = Date.now(); @@ -330,58 +414,71 @@ abstract class PersistentConnection extends Disposable { attempt++; const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]); try { - this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime)); - await sleep(waitTime); + const sleepPromise = sleep(waitTime); + this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise)); + + this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); + try { + await sleepPromise; + } catch { } // User canceled timer + + if (PersistentConnection._permanentFailure) { + this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`); + break; + } // connection was lost, let's try to re-establish it this._onDidStateChange.fire(new ReconnectionRunningEvent()); + this._options.logService.info(`${logPrefix} resolving connection...`); const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol); - await connectWithTimeLimit(this._reconnect(simpleOptions), 30 * 1000 /*30s*/); + this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`); + await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), 30 * 1000 /*30s*/); + this._options.logService.info(`${logPrefix} reconnected!`); this._onDidStateChange.fire(new ConnectionGainEvent()); break; } catch (err) { if (err.code === 'VSCODE_CONNECTION_ERROR') { - console.error(`A permanent connection error occurred`); - console.error(err); - this._permanentFailure = true; - this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); - this.protocol.acceptDisconnect(); + this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`); + this._options.logService.error(err); + PersistentConnection.triggerPermanentFailure(); break; } if (Date.now() - disconnectStartTime > ProtocolConstants.ReconnectionGraceTime) { - console.error(`Giving up after reconnection grace time has expired!`); - this._permanentFailure = true; - this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); - this.protocol.acceptDisconnect(); + this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time has expired! Will give up now! Error:`); + this._options.logService.error(err); + PersistentConnection.triggerPermanentFailure(); break; } if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) { - console.warn(`A temporarily not available error occured while trying to reconnect:`); - console.warn(err); + this._options.logService.info(`${logPrefix} A temporarily not available error occured while trying to reconnect, will try again...`); + this._options.logService.trace(err); // try again! continue; } if ((err.code === 'ETIMEDOUT' || err.code === 'ENETUNREACH' || err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') && err.syscall === 'connect') { - console.warn(`A connect error occured while trying to reconnect:`); - console.warn(err); + this._options.logService.info(`${logPrefix} A network error occured while trying to reconnect, will try again...`); + this._options.logService.trace(err); // try again! continue; } if (isPromiseCanceledError(err)) { - console.warn(`A cancel error occured while trying to reconnect:`); - console.warn(err); + this._options.logService.info(`${logPrefix} A promise cancelation error occured while trying to reconnect, will try again...`); + this._options.logService.trace(err); // try again! continue; } - console.error(`An error occured while trying to reconnect:`); - console.error(err); - this._permanentFailure = true; - this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); - this.protocol.acceptDisconnect(); + this._options.logService.error(`${logPrefix} An unknown error occured while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); + this._options.logService.error(err); + PersistentConnection.triggerPermanentFailure(); break; } - } while (!this._permanentFailure); + } while (!PersistentConnection._permanentFailure); + } + + private _gotoPermanentFailure(): void { + this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); + safeDisposeProtocolAndSocket(this.protocol); } protected abstract _reconnect(options: ISimpleConnectionOptions): Promise; @@ -392,7 +489,7 @@ export class ManagementPersistentConnection extends PersistentConnection { public readonly client: Client; constructor(options: IConnectionOptions, remoteAuthority: string, clientId: string, reconnectionToken: string, protocol: PersistentProtocol) { - super(options, reconnectionToken, protocol); + super(ConnectionType.Management, options, reconnectionToken, protocol); this.client = this._register(new Client(protocol, { remoteAuthority: remoteAuthority, clientId: clientId @@ -410,7 +507,7 @@ export class ExtensionHostPersistentConnection extends PersistentConnection { public readonly debugPort: number | undefined; constructor(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams, reconnectionToken: string, protocol: PersistentProtocol, debugPort: number | undefined) { - super(options, reconnectionToken, protocol); + super(ConnectionType.ExtensionHost, options, reconnectionToken, protocol); this._startArguments = startArguments; this.debugPort = debugPort; } @@ -420,17 +517,19 @@ export class ExtensionHostPersistentConnection extends PersistentConnection { } } -function connectWithTimeLimit(p: Promise, timeLimit: number): Promise { - return new Promise((resolve, reject) => { +function connectWithTimeLimit(logService: ILogService, p: Promise, timeLimit: number): Promise { + return new Promise((resolve, reject) => { let timeout = setTimeout(() => { const err: any = new Error('Time limit reached'); err.code = 'ETIMEDOUT'; err.syscall = 'connect'; + logService.error(`[remote-connection] The time limit has been reached for a connection. Error:`); + logService.error(err); reject(err); }, timeLimit); - p.then(() => { + p.then((value) => { clearTimeout(timeout); - resolve(); + resolve(value); }, (err) => { clearTimeout(timeout); reject(err); @@ -438,6 +537,17 @@ function connectWithTimeLimit(p: Promise, timeLimit: number): Promise { this.logService.trace('RequestService#request', options.url); - const authorization = this.configurationService.getValue('http.proxyAuthorization'); - if (authorization) { - options.headers = assign(options.headers || {}, { 'Proxy-Authorization': authorization }); + if (!options.proxyAuthorization) { + options.proxyAuthorization = this.configurationService.getValue('http.proxyAuthorization'); } - const xhr = new XMLHttpRequest(); - return new Promise((resolve, reject) => { - - xhr.open(options.type || 'GET', options.url || '', true, options.user, options.password); - this.setRequestHeaders(xhr, options); - - xhr.responseType = 'arraybuffer'; - xhr.onerror = e => reject(new Error(xhr.statusText && ('XHR failed: ' + xhr.statusText))); - xhr.onload = (e) => { - resolve({ - res: { - statusCode: xhr.status, - headers: this.getResponseHeaders(xhr) - }, - stream: bufferToStream(VSBuffer.wrap(new Uint8Array(xhr.response))) - }); - }; - xhr.ontimeout = e => reject(new Error(`XHR timeout: ${options.timeout}ms`)); - - if (options.timeout) { - xhr.timeout = options.timeout; - } - - xhr.send(options.data); - - // cancel - token.onCancellationRequested(() => { - xhr.abort(); - reject(canceled()); - }); - }); + return request(options, token); } - - private setRequestHeaders(xhr: XMLHttpRequest, options: IRequestOptions): void { - if (options.headers) { - outer: for (let k in options.headers) { - switch (k) { - case 'User-Agent': - case 'Accept-Encoding': - case 'Content-Length': - // unsafe headers - continue outer; - } - xhr.setRequestHeader(k, options.headers[k]); - - } - } - } - - private getResponseHeaders(xhr: XMLHttpRequest): { [name: string]: string } { - const headers: { [name: string]: string } = Object.create(null); - for (const line of xhr.getAllResponseHeaders().split(/\r\n|\n|\r/g)) { - if (line) { - const idx = line.indexOf(':'); - headers[line.substr(0, idx).trim().toLowerCase()] = line.substr(idx + 1).trim(); - } - } - return headers; - } - -} \ No newline at end of file +} diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 31e3c314242c1..7ec2f5f3742f3 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -8,33 +8,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { VSBufferReadableStream, streamToBuffer } from 'vs/base/common/buffer'; +import { streamToBuffer } from 'vs/base/common/buffer'; +import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; export const IRequestService = createDecorator('requestService'); -export interface IHeaders { - [header: string]: string; -} - -export interface IRequestOptions { - type?: string; - url?: string; - user?: string; - password?: string; - headers?: IHeaders; - timeout?: number; - data?: string; - followRedirects?: number; -} - -export interface IRequestContext { - res: { - headers: IHeaders; - statusCode?: number; - }; - stream: VSBufferReadableStream; -} - export interface IRequestService { _serviceBrand: any; diff --git a/src/vs/platform/request/common/requestIpc.ts b/src/vs/platform/request/common/requestIpc.ts index 8f55326170771..22acbce9dee06 100644 --- a/src/vs/platform/request/common/requestIpc.ts +++ b/src/vs/platform/request/common/requestIpc.ts @@ -5,7 +5,8 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IRequestService, IRequestOptions, IRequestContext, IHeaders } from 'vs/platform/request/common/request'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; import { VSBuffer, bufferToStream, streamToBuffer } from 'vs/base/common/buffer'; diff --git a/src/vs/platform/request/electron-main/requestService.ts b/src/vs/platform/request/electron-main/requestService.ts index cd27b9fbf5d43..de9eacfd44794 100644 --- a/src/vs/platform/request/electron-main/requestService.ts +++ b/src/vs/platform/request/electron-main/requestService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRequestOptions, IRequestContext } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { RequestService as NodeRequestService, IRawRequestFunction } from 'vs/platform/request/node/requestService'; import { assign } from 'vs/base/common/objects'; import { net } from 'electron'; diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 8ca875f6fa9fd..07a32d78f448c 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -13,7 +13,8 @@ import { assign } from 'vs/base/common/objects'; import { isBoolean, isNumber } from 'vs/base/common/types'; import { canceled } from 'vs/base/common/errors'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IRequestOptions, IRequestContext, IRequestService, IHTTPConfiguration } from 'vs/platform/request/common/request'; +import { IRequestService, IHTTPConfiguration } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { getProxyAgent, Agent } from 'vs/platform/request/node/proxy'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 392ca58f2b88b..5648772339f11 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -5,15 +5,17 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, FileStorageDatabase } from 'vs/platform/storage/common/storage'; +import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IStorage, Storage } from 'vs/base/parts/storage/common/storage'; +import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; +import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; -import { runWhenIdle } from 'vs/base/common/async'; +import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async'; +import { serializableToMap, mapToSerializable } from 'vs/base/common/map'; +import { VSBuffer } from 'vs/base/common/buffer'; export class BrowserStorageService extends Disposable implements IStorageService { @@ -35,6 +37,7 @@ export class BrowserStorageService extends Disposable implements IStorageService private workspaceStorageFile: URI; private initializePromise: Promise; + private periodicSaveScheduler = this._register(new RunOnceScheduler(() => this.collectState(), 5000)); get hasPendingUpdate(): boolean { return this.globalStorageDatabase.hasPendingUpdate || this.workspaceStorageDatabase.hasPendingUpdate; @@ -51,20 +54,28 @@ export class BrowserStorageService extends Disposable implements IStorageService // long running operation. // Instead, periodically ask customers to save save. The library will be clever enough // to only save state that has actually changed. - this.saveStatePeriodically(); + this.periodicSaveScheduler.schedule(); } - private saveStatePeriodically(): void { - setTimeout(() => { - runWhenIdle(() => { - - // this event will potentially cause new state to be stored + private collectState(): void { + runWhenIdle(() => { + + // this event will potentially cause new state to be stored + // since new state will only be created while the document + // has focus, one optimization is to not run this when the + // document has no focus, assuming that state has not changed + // + // another optimization is to not collect more state if we + // have a pending update already running which indicates + // that the connection is either slow or disconnected and + // thus unhealthy. + if (document.hasFocus() && !this.hasPendingUpdate) { this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE }); + } - // repeat - this.saveStatePeriodically(); - }); - }, 5000); + // repeat + this.periodicSaveScheduler.schedule(); + }); } initialize(payload: IWorkspaceInitializationPayload): Promise { @@ -83,14 +94,14 @@ export class BrowserStorageService extends Disposable implements IStorageService // Workspace Storage this.workspaceStorageFile = joinPath(stateRoot, `${payload.id}.json`); - this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, this.fileService)); - this.workspaceStorage = new Storage(this.workspaceStorageDatabase); + this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService)); + this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase)); this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE }))); // Global Storage this.globalStorageFile = joinPath(stateRoot, 'global.json'); - this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, this.fileService)); - this.globalStorage = new Storage(this.globalStorageDatabase); + this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService)); + this.globalStorage = this._register(new Storage(this.globalStorageDatabase)); this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL }))); // Init both @@ -140,12 +151,127 @@ export class BrowserStorageService extends Disposable implements IStorageService } close(): void { + // We explicitly do not close our DBs because writing data onBeforeUnload() + // can result in unexpected results. Namely, it seems that - even though this + // operation is async - sometimes it is being triggered on unload and + // succeeds. Often though, the DBs turn out to be empty because the write + // never had a chance to complete. + // + // Instead we trigger dispose() to ensure that no timeouts or callbacks + // get triggered in this phase. + this.dispose(); + } +} + +export class FileStorageDatabase extends Disposable implements IStorageDatabase { + + private readonly _onDidChangeItemsExternal: Emitter = this._register(new Emitter()); + readonly onDidChangeItemsExternal: Event = this._onDidChangeItemsExternal.event; + + private cache: Map | undefined; + + private pendingUpdate: Promise = Promise.resolve(); - // Signal as event so that clients can still store data - this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); + private _hasPendingUpdate = false; + get hasPendingUpdate(): boolean { + return this._hasPendingUpdate; + } + + private isWatching = false; + + constructor( + private readonly file: URI, + private readonly watchForExternalChanges: boolean, + @IFileService private readonly fileService: IFileService + ) { + super(); + } + + private async ensureWatching(): Promise { + if (this.isWatching || !this.watchForExternalChanges) { + return; + } + + const exists = await this.fileService.exists(this.file); + if (this.isWatching || !exists) { + return; // file must exist to be watched + } + + this.isWatching = true; + + this._register(this.fileService.watch(this.file)); + this._register(this.fileService.onFileChanges(e => { + if (document.hasFocus()) { + return; // optimization: ignore changes from ourselves by checking for focus + } + + if (!e.contains(this.file, FileChangeType.UPDATED)) { + return; // not our file + } + + this.onDidStorageChangeExternal(); + })); + } + + private async onDidStorageChangeExternal(): Promise { + const items = await this.doGetItemsFromFile(); + + this.cache = items; + + this._onDidChangeItemsExternal.fire({ items }); + } + + async getItems(): Promise> { + if (!this.cache) { + try { + this.cache = await this.doGetItemsFromFile(); + } catch (error) { + this.cache = new Map(); + } + } + + return this.cache; + } + + private async doGetItemsFromFile(): Promise> { + await this.pendingUpdate; + + const itemsRaw = await this.fileService.readFile(this.file); + + this.ensureWatching(); // now that the file must exist, ensure we watch it for changes + + return serializableToMap(JSON.parse(itemsRaw.value.toString())); + } + + async updateItems(request: IUpdateRequest): Promise { + const items = await this.getItems(); + + if (request.insert) { + request.insert.forEach((value, key) => items.set(key, value)); + } + + if (request.delete) { + request.delete.forEach(key => items.delete(key)); + } + + await this.pendingUpdate; + + this.pendingUpdate = (async () => { + try { + this._hasPendingUpdate = true; + + await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items)))); + + this.ensureWatching(); // now that the file must exist, ensure we watch it for changes + } finally { + this._hasPendingUpdate = false; + } + })(); + + return this.pendingUpdate; + } - // Close DBs - this.globalStorage.close(); - this.workspaceStorage.close(); + close(): Promise { + return this.pendingUpdate; } } diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 46fdd613d11c8..8860535a54a65 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -7,11 +7,6 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/co import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IUpdateRequest, IStorageDatabase } from 'vs/base/parts/storage/common/storage'; -import { serializableToMap, mapToSerializable } from 'vs/base/common/map'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { URI } from 'vs/base/common/uri'; -import { IFileService } from 'vs/platform/files/common/files'; export const IStorageService = createDecorator('storageService'); @@ -212,75 +207,6 @@ export class InMemoryStorageService extends Disposable implements IStorageServic } } -export class FileStorageDatabase extends Disposable implements IStorageDatabase { - - readonly onDidChangeItemsExternal = Event.None; // TODO@Ben implement global UI storage events - - private cache: Map | undefined; - - private pendingUpdate: Promise = Promise.resolve(); - - private _hasPendingUpdate = false; - get hasPendingUpdate(): boolean { - return this._hasPendingUpdate; - } - - constructor( - private readonly file: URI, - private readonly fileService: IFileService - ) { - super(); - } - - async getItems(): Promise> { - if (!this.cache) { - try { - this.cache = await this.doGetItemsFromFile(); - } catch (error) { - this.cache = new Map(); - } - } - - return this.cache; - } - - private async doGetItemsFromFile(): Promise> { - await this.pendingUpdate; - - const itemsRaw = await this.fileService.readFile(this.file); - - return serializableToMap(JSON.parse(itemsRaw.value.toString())); - } - - async updateItems(request: IUpdateRequest): Promise { - const items = await this.getItems(); - - if (request.insert) { - request.insert.forEach((value, key) => items.set(key, value)); - } - - if (request.delete) { - request.delete.forEach(key => items.delete(key)); - } - - await this.pendingUpdate; - - this._hasPendingUpdate = true; - - this.pendingUpdate = this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items)))) - .then(() => undefined) - .finally(() => { - this._hasPendingUpdate = false; - }); - - return this.pendingUpdate; - } - - close(): Promise { - return this.pendingUpdate; - } -} - export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { const safeParse = (value: string) => { try { diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index aaba475a2695f..8ed04953e41ca 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -5,7 +5,7 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event, Emitter } from 'vs/base/common/event'; -import { StorageMainService, IStorageChangeEvent } from 'vs/platform/storage/node/storageMainService'; +import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService'; import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -38,7 +38,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC constructor( private logService: ILogService, - private storageMainService: StorageMainService + private storageMainService: IStorageMainService ) { super(); @@ -203,4 +203,4 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS dispose(this.onDidChangeItemsOnMainListener); } -} \ No newline at end of file +} diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index d1f06b6ecb200..2f2dd9e3bd2df 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -34,6 +34,16 @@ export interface IStorageMainService { */ readonly onWillSaveState: Event; + /** + * Access to all cached items of this storage service. + */ + readonly items: Map; + + /** + * Required call to ensure the service can be used. + */ + initialize(): Promise; + /** * Retrieve an element stored with the given key from storage. Use * the provided defaultValue if the element is null or undefined. diff --git a/src/vs/platform/storage/test/node/storage.test.ts b/src/vs/platform/storage/test/electron-browser/storage.test.ts similarity index 92% rename from src/vs/platform/storage/test/node/storage.test.ts rename to src/vs/platform/storage/test/electron-browser/storage.test.ts index 7c77b9e1c08ff..4e5e10a4e13ab 100644 --- a/src/vs/platform/storage/test/node/storage.test.ts +++ b/src/vs/platform/storage/test/electron-browser/storage.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equal } from 'assert'; -import { FileStorageDatabase } from 'vs/platform/storage/common/storage'; +import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; @@ -49,7 +49,7 @@ suite('Storage', () => { }); test('File Based Storage', async () => { - let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), fileService)); + let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService)); await storage.init(); @@ -63,7 +63,7 @@ suite('Storage', () => { await storage.close(); - storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), fileService)); + storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService)); await storage.init(); @@ -81,7 +81,7 @@ suite('Storage', () => { await storage.close(); - storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), fileService)); + storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService)); await storage.init(); @@ -89,4 +89,4 @@ suite('Storage', () => { equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts b/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts index 3e6c88bd45103..ac0da4361f762 100644 --- a/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts +++ b/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts @@ -12,21 +12,23 @@ export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; import * as Platform from 'vs/base/common/platform'; import * as uuid from 'vs/base/common/uuid'; +import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtils'; export async function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string | undefined, version: string | undefined, machineId: string, remoteAuthority?: string): Promise<{ [name: string]: string | undefined }> { const result: { [name: string]: string | undefined; } = Object.create(null); - const instanceId = storageService.get(instanceStorageKey, StorageScope.GLOBAL)!; const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; + /** + * Note: In the web, session date information is fetched from browser storage, so these dates are tied to a specific + * browser and not the machine overall. + */ // __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.firstSessionDate'] = firstSessionDate; // __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.lastSessionDate'] = lastSessionDate || ''; // __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.isNewSession'] = !lastSessionDate ? '1' : '0'; - // __GDPR__COMMON__ "common.instanceId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.instanceId'] = instanceId; // __GDPR__COMMON__ "common.remoteAuthority" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.remoteAuthority'] = cleanRemoteAuthority(remoteAuthority); @@ -69,18 +71,3 @@ export async function resolveWorkbenchCommonProperties(storageService: IStorageS return result; } -function cleanRemoteAuthority(remoteAuthority?: string): string { - if (!remoteAuthority) { - return 'none'; - } - - let ret = 'other'; - // Whitelisted remote authorities - ['ssh-remote', 'dev-container', 'wsl'].forEach((res: string) => { - if (remoteAuthority!.indexOf(`${res}+`) === 0) { - ret = res; - } - }); - - return ret; -} diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index f8990d64a32ae..d98abe9e15684 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -288,6 +288,22 @@ export function validateTelemetryData(data?: any): { properties: Properties, mea }; } +export function cleanRemoteAuthority(remoteAuthority?: string): string { + if (!remoteAuthority) { + return 'none'; + } + + let ret = 'other'; + // Whitelisted remote authorities + ['ssh-remote', 'dev-container', 'attached-container', 'wsl'].forEach((res: string) => { + if (remoteAuthority!.indexOf(`${res}+`) === 0) { + ret = res; + } + }); + + return ret; +} + function flatten(obj: any, result: { [key: string]: any }, order: number = 0, prefix?: string): void { if (!obj) { return; diff --git a/src/vs/platform/telemetry/node/workbenchCommonProperties.ts b/src/vs/platform/telemetry/node/workbenchCommonProperties.ts index d1b83a1414611..81d6a4d3d2289 100644 --- a/src/vs/platform/telemetry/node/workbenchCommonProperties.ts +++ b/src/vs/platform/telemetry/node/workbenchCommonProperties.ts @@ -6,6 +6,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtils'; export async function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string | undefined, version: string | undefined, machineId: string, installSourcePath: string, remoteAuthority?: string): Promise<{ [name: string]: string | undefined }> { const result = await resolveCommonProperties(commit, version, machineId, installSourcePath); @@ -30,19 +31,3 @@ export async function resolveWorkbenchCommonProperties(storageService: IStorageS return result; } - -function cleanRemoteAuthority(remoteAuthority?: string): string { - if (!remoteAuthority) { - return 'none'; - } - - let ret = 'other'; - // Whitelisted remote authorities - ['ssh-remote', 'dev-container', 'wsl'].forEach((res: string) => { - if (remoteAuthority!.indexOf(`${res}+`) === 0) { - ret = res; - } - }); - - return ret; -} diff --git a/src/vs/platform/url/common/url.ts b/src/vs/platform/url/common/url.ts index f73971c4e00bc..87a5a8fbeecc2 100644 --- a/src/vs/platform/url/common/url.ts +++ b/src/vs/platform/url/common/url.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable } from 'vs/base/common/lifecycle'; export const IURLService = createDecorator('urlService'); @@ -14,8 +14,17 @@ export interface IURLHandler { } export interface IURLService { - _serviceBrand: any; + + _serviceBrand: ServiceIdentifier; + + /** + * Create a URL that can be called to trigger IURLhandlers. + * The URL that gets passed to the IURLHandlers carries over + * any of the provided IURLCreateOption values. + */ + create(options?: Partial): URI; open(url: URI): Promise; + registerHandler(handler: IURLHandler): IDisposable; } diff --git a/src/vs/platform/url/common/urlService.ts b/src/vs/platform/url/common/urlService.ts index 66d4abd6722a5..eb216a6b33555 100644 --- a/src/vs/platform/url/common/urlService.ts +++ b/src/vs/platform/url/common/urlService.ts @@ -4,18 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; -import { URI } from 'vs/base/common/uri'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { first } from 'vs/base/common/async'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { values } from 'vs/base/common/map'; +import { first } from 'vs/base/common/async'; +import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -export class URLService implements IURLService { +export abstract class AbstractURLService extends Disposable implements IURLService { _serviceBrand!: ServiceIdentifier; private handlers = new Set(); + abstract create(options?: Partial): URI; + open(uri: URI): Promise { const handlers = values(this.handlers); return first(handlers.map(h => () => h.handleURL(uri)), undefined, false).then(val => val || false); diff --git a/src/vs/platform/url/node/urlIpc.ts b/src/vs/platform/url/node/urlIpc.ts index 2ced486d02d08..0e70e9c6534a7 100644 --- a/src/vs/platform/url/node/urlIpc.ts +++ b/src/vs/platform/url/node/urlIpc.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; @@ -39,6 +39,10 @@ export class URLServiceChannelClient implements IURLService { registerHandler(handler: IURLHandler): IDisposable { throw new Error('Not implemented.'); } + + create(_options?: Partial): URI { + throw new Error('Method not implemented.'); + } } export class URLHandlerChannel implements IServerChannel { diff --git a/src/vs/platform/url/node/urlService.ts b/src/vs/platform/url/node/urlService.ts new file mode 100644 index 0000000000000..adf857493e848 --- /dev/null +++ b/src/vs/platform/url/node/urlService.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI, UriComponents } from 'vs/base/common/uri'; +import product from 'vs/platform/product/node/product'; +import { AbstractURLService } from 'vs/platform/url/common/urlService'; + +export class URLService extends AbstractURLService { + + create(options?: Partial): URI { + const { authority, path, query, fragment } = options ? options : { authority: undefined, path: undefined, query: undefined, fragment: undefined }; + + return URI.from({ scheme: product.urlProtocol, authority, path, query, fragment }); + } +} diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index a1f50a5a715db..bca1181e59197 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -96,12 +96,12 @@ export interface IWindowsService { _serviceBrand: any; - onWindowOpen: Event; - onWindowFocus: Event; - onWindowBlur: Event; - onWindowMaximize: Event; - onWindowUnmaximize: Event; - onRecentlyOpenedChange: Event; + readonly onWindowOpen: Event; + readonly onWindowFocus: Event; + readonly onWindowBlur: Event; + readonly onWindowMaximize: Event; + readonly onWindowUnmaximize: Event; + readonly onRecentlyOpenedChange: Event; // Dialogs pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise; @@ -240,6 +240,8 @@ export interface IWindowService { closeWorkspace(): Promise; updateTouchBar(items: ISerializableCommandAction[][]): Promise; enterWorkspace(path: URI): Promise; + // rationale: will eventually move to electron-browser + // tslint:disable-next-line: no-dom-globals toggleFullScreen(target?: HTMLElement): Promise; setRepresentedFilename(fileName: string): Promise; getRecentlyOpened(): Promise; diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts similarity index 94% rename from src/vs/platform/windows/node/windowsIpc.ts rename to src/vs/platform/windows/common/windowsIpc.ts index 6fbad27a5497b..3cdf71e2f4ab2 100644 --- a/src/vs/platform/windows/node/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -12,14 +12,14 @@ import { IRecent, isRecentFile, isRecentFolder } from 'vs/platform/history/commo export class WindowsChannel implements IServerChannel { - private onWindowOpen: Event; - private onWindowFocus: Event; - private onWindowBlur: Event; - private onWindowMaximize: Event; - private onWindowUnmaximize: Event; - private onRecentlyOpenedChange: Event; + private readonly onWindowOpen: Event; + private readonly onWindowFocus: Event; + private readonly onWindowBlur: Event; + private readonly onWindowMaximize: Event; + private readonly onWindowUnmaximize: Event; + private readonly onRecentlyOpenedChange: Event; - constructor(private service: IWindowsService) { + constructor(private readonly service: IWindowsService) { this.onWindowOpen = Event.buffer(service.onWindowOpen, true); this.onWindowFocus = Event.buffer(service.onWindowFocus, true); this.onWindowBlur = Event.buffer(service.onWindowBlur, true); @@ -120,4 +120,4 @@ export class WindowsChannel implements IServerChannel { throw new Error(`Call not found: ${command}`); } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/electron-browser/windowsService.ts b/src/vs/platform/windows/electron-browser/windowsService.ts index 228dfd98ba259..57bd96143af12 100644 --- a/src/vs/platform/windows/electron-browser/windowsService.ts +++ b/src/vs/platform/windows/electron-browser/windowsService.ts @@ -13,17 +13,14 @@ import { URI } from 'vs/base/common/uri'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export class WindowsService implements IWindowsService { - _serviceBrand: any; + _serviceBrand!: ServiceIdentifier; private channel: IChannel; - constructor(@IMainProcessService mainProcessService: IMainProcessService) { - this.channel = mainProcessService.getChannel('windows'); - } - get onWindowOpen(): Event { return this.channel.listen('onWindowOpen'); } get onWindowFocus(): Event { return this.channel.listen('onWindowFocus'); } get onWindowBlur(): Event { return this.channel.listen('onWindowBlur'); } @@ -31,6 +28,10 @@ export class WindowsService implements IWindowsService { get onWindowUnmaximize(): Event { return this.channel.listen('onWindowUnmaximize'); } get onRecentlyOpenedChange(): Event { return this.channel.listen('onRecentlyOpenedChange'); } + constructor(@IMainProcessService mainProcessService: IMainProcessService) { + this.channel = mainProcessService.getChannel('windows'); + } + pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise { return this.channel.call('pickFileFolderAndOpen', options); } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 6d3f1024f3e04..16b3f1d60d75e 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -17,6 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { dirname, joinPath } from 'vs/base/common/resources'; suite('WorkspacesMainService', () => { const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice'); @@ -51,13 +52,13 @@ suite('WorkspacesMainService', () => { let service: TestWorkspacesMainService; - setup(() => { + setup(async () => { service = new TestWorkspacesMainService(environmentService, logService); // Delete any existing backups completely and then re-create it. - return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE).then(() => { - return pfs.mkdirp(untitledWorkspacesHomePath); - }); + await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + + return pfs.mkdirp(untitledWorkspacesHomePath); }); teardown(() => { @@ -77,57 +78,50 @@ suite('WorkspacesMainService', () => { assert.equal(u1.toString(), u2.toString()); } - test('createWorkspace (folders)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - assert.ok(workspace); - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - assert.ok(service.isUntitledWorkspace(workspace)); - - const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); // - assertPathEquals((ws.folders[0]).path, process.cwd()); - assertPathEquals((ws.folders[1]).path, os.tmpdir()); + test('createWorkspace (folders)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + assert.ok(workspace); + assert.ok(fs.existsSync(workspace.configPath.fsPath)); + assert.ok(service.isUntitledWorkspace(workspace)); - assert.ok(!(ws.folders[0]).name); - assert.ok(!(ws.folders[1]).name); - }); + const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); + assert.equal(ws.folders.length, 2); + assertPathEquals((ws.folders[0]).path, process.cwd()); + assertPathEquals((ws.folders[1]).path, os.tmpdir()); + assert.ok(!(ws.folders[0]).name); + assert.ok(!(ws.folders[1]).name); }); - test('createWorkspace (folders with name)', () => { - return createWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']).then(workspace => { - assert.ok(workspace); - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - assert.ok(service.isUntitledWorkspace(workspace)); - - const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); // - assertPathEquals((ws.folders[0]).path, process.cwd()); - assertPathEquals((ws.folders[1]).path, os.tmpdir()); + test('createWorkspace (folders with name)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']); + assert.ok(workspace); + assert.ok(fs.existsSync(workspace.configPath.fsPath)); + assert.ok(service.isUntitledWorkspace(workspace)); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); - }); + const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); + assert.equal(ws.folders.length, 2); + assertPathEquals((ws.folders[0]).path, process.cwd()); + assertPathEquals((ws.folders[1]).path, os.tmpdir()); + assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); + assert.equal((ws.folders[1]).name, 'tempdir'); }); - test('createUntitledWorkspace (folders as other resource URIs)', () => { + test('createUntitledWorkspace (folders as other resource URIs)', async () => { const folder1URI = URI.parse('myscheme://server/work/p/f1'); const folder2URI = URI.parse('myscheme://server/work/o/f3'); - return service.createUntitledWorkspace([{ uri: folder1URI }, { uri: folder2URI }], 'server').then(workspace => { - assert.ok(workspace); - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - assert.ok(service.isUntitledWorkspace(workspace)); - - const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); - - assert.ok(!(ws.folders[0]).name); - assert.ok(!(ws.folders[1]).name); + const workspace = await service.createUntitledWorkspace([{ uri: folder1URI }, { uri: folder2URI }], 'server'); + assert.ok(workspace); + assert.ok(fs.existsSync(workspace.configPath.fsPath)); + assert.ok(service.isUntitledWorkspace(workspace)); - assert.equal(ws.remoteAuthority, 'server'); - }); + const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); + assert.equal(ws.folders.length, 2); + assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); + assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.ok(!(ws.folders[0]).name); + assert.ok(!(ws.folders[1]).name); + assert.equal(ws.remoteAuthority, 'server'); }); test('createWorkspaceSync (folders)', () => { @@ -178,145 +172,130 @@ suite('WorkspacesMainService', () => { assert.ok(!(ws.folders[1]).name); }); - test('resolveWorkspaceSync', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath)); + test('resolveWorkspaceSync', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath)); - // make it a valid workspace path - const newPath = path.join(path.dirname(workspace.configPath.fsPath), `workspace.${WORKSPACE_EXTENSION}`); - fs.renameSync(workspace.configPath.fsPath, newPath); - workspace.configPath = URI.file(newPath); + // make it a valid workspace path + const newPath = path.join(path.dirname(workspace.configPath.fsPath), `workspace.${WORKSPACE_EXTENSION}`); + fs.renameSync(workspace.configPath.fsPath, newPath); + workspace.configPath = URI.file(newPath); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.equal(2, resolved!.folders.length); - assertEqualURI(resolved!.configPath, workspace.configPath); - assert.ok(resolved!.id); + const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + assert.equal(2, resolved!.folders.length); + assertEqualURI(resolved!.configPath, workspace.configPath); + assert.ok(resolved!.id); + fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace - fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace - const resolvedInvalid = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.ok(!resolvedInvalid); - }); + const resolvedInvalid = service.resolveLocalWorkspaceSync(workspace.configPath); + assert.ok(!resolvedInvalid); }); - test('resolveWorkspaceSync (support relative paths)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] })); + test('resolveWorkspaceSync (support relative paths)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] })); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); - }); + const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); }); - test('resolveWorkspaceSync (support relative paths #2)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] })); + test('resolveWorkspaceSync (support relative paths #2)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] })); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'other'))); - }); + const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'other'))); }); - test('resolveWorkspaceSync (support relative paths #3)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] })); + test('resolveWorkspaceSync (support relative paths #3)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] })); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); - }); + const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); }); - test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma + test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); - }); + const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); }); - test('rewriteWorkspaceFileForNewLocation', () => { + test('rewriteWorkspaceFileForNewLocation', async () => { const folder1 = process.cwd(); // absolute path because outside of tmpDir const tmpDir = os.tmpdir(); const tmpInsideDir = path.join(os.tmpdir(), 'inside'); - return createWorkspace([folder1, tmpInsideDir, path.join(tmpInsideDir, 'somefolder')]).then(workspace => { - const origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - - let origConfigPath = workspace.configPath; - let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); - let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, workspaceConfigPath); - - let ws = JSON.parse(newContent) as IStoredWorkspace; - assert.equal(ws.folders.length, 3); - assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir - assertPathEquals((ws.folders[1]).path, '.'); - assertPathEquals((ws.folders[2]).path, 'somefolder'); - - origConfigPath = workspaceConfigPath; - workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); - - ws = JSON.parse(newContent) as IStoredWorkspace; - assert.equal(ws.folders.length, 3); - assertPathEquals((ws.folders[0]).path, folder1); - assertPathEquals((ws.folders[1]).path, 'inside'); - assertPathEquals((ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder'); - - origConfigPath = workspaceConfigPath; - workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); - - ws = JSON.parse(newContent) as IStoredWorkspace; - assert.equal(ws.folders.length, 3); - assertPathEquals((ws.folders[0]).path, folder1); - assertPathEquals((ws.folders[1]).path, tmpInsideDir); - assertPathEquals((ws.folders[2]).path, path.join(tmpInsideDir, 'somefolder')); - - origConfigPath = workspaceConfigPath; - workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); - - ws = JSON.parse(newContent) as IStoredWorkspace; - assert.equal(ws.folders.length, 3); - assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); - assert.equal((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); - assert.equal((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); - - service.deleteUntitledWorkspaceSync(workspace); - }); + const workspace = await createWorkspace([folder1, tmpInsideDir, path.join(tmpInsideDir, 'somefolder')]); + const origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); + + let origConfigPath = workspace.configPath; + let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, workspaceConfigPath); + let ws = (JSON.parse(newContent) as IStoredWorkspace); + assert.equal(ws.folders.length, 3); + assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir + assertPathEquals((ws.folders[1]).path, '.'); + assertPathEquals((ws.folders[2]).path, 'somefolder'); + + origConfigPath = workspaceConfigPath; + workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); + ws = (JSON.parse(newContent) as IStoredWorkspace); + assert.equal(ws.folders.length, 3); + assertPathEquals((ws.folders[0]).path, folder1); + assertPathEquals((ws.folders[1]).path, 'inside'); + assertPathEquals((ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder'); + + origConfigPath = workspaceConfigPath; + workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); + ws = (JSON.parse(newContent) as IStoredWorkspace); + assert.equal(ws.folders.length, 3); + assertPathEquals((ws.folders[0]).path, folder1); + assertPathEquals((ws.folders[1]).path, tmpInsideDir); + assertPathEquals((ws.folders[2]).path, path.join(tmpInsideDir, 'somefolder')); + + origConfigPath = workspaceConfigPath; + workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); + ws = (JSON.parse(newContent) as IStoredWorkspace); + assert.equal(ws.folders.length, 3); + assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); + assert.equal((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); + assert.equal((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); + + service.deleteUntitledWorkspaceSync(workspace); }); - test('rewriteWorkspaceFileForNewLocation (preserves comments)', () => { - return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => { - const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); - - let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - origContent = `// this is a comment\n${origContent}`; - - let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + test('rewriteWorkspaceFileForNewLocation (preserves comments)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]); + const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); - assert.equal(0, newContent.indexOf('// this is a comment')); + let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); + origContent = `// this is a comment\n${origContent}`; - service.deleteUntitledWorkspaceSync(workspace); - }); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + assert.equal(0, newContent.indexOf('// this is a comment')); + service.deleteUntitledWorkspaceSync(workspace); }); - test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', () => { - return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => { - const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); - let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash + test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]); + const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); + origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash - const ws = JSON.parse(newContent) as IStoredWorkspace; - assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); - - service.deleteUntitledWorkspaceSync(workspace); - }); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + const ws = (JSON.parse(newContent) as IStoredWorkspace); + assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); + service.deleteUntitledWorkspaceSync(workspace); }); - test('rewriteWorkspaceFileForNewLocation (unc paths)', () => { + test('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { if (!isWindows) { return Promise.resolve(); } @@ -326,68 +305,59 @@ suite('WorkspacesMainService', () => { const folder2Location = '\\\\server\\share2\\some\\path'; const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more'); - return createWorkspace([folder1Location, folder2Location, folder3Location]).then(workspace => { - const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); - let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + const workspace = await createWorkspace([folder1Location, folder2Location, folder3Location]); + const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); + let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + const ws = (JSON.parse(newContent) as IStoredWorkspace); + assertPathEquals((ws.folders[0]).path, folder1Location); + assertPathEquals((ws.folders[1]).path, folder2Location); + assertPathEquals((ws.folders[2]).path, 'inner\\more'); - const ws = JSON.parse(newContent) as IStoredWorkspace; - assertPathEquals((ws.folders[0]).path, folder1Location); - assertPathEquals((ws.folders[1]).path, folder2Location); - assertPathEquals((ws.folders[2]).path, 'inner\\more'); - - service.deleteUntitledWorkspaceSync(workspace); - }); + service.deleteUntitledWorkspaceSync(workspace); }); - test('deleteUntitledWorkspaceSync (untitled)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - - service.deleteUntitledWorkspaceSync(workspace); - - assert.ok(!fs.existsSync(workspace.configPath.fsPath)); - }); + test('deleteUntitledWorkspaceSync (untitled)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + assert.ok(fs.existsSync(workspace.configPath.fsPath)); + service.deleteUntitledWorkspaceSync(workspace); + assert.ok(!fs.existsSync(workspace.configPath.fsPath)); }); - test('deleteUntitledWorkspaceSync (saved)', () => { - return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { - service.deleteUntitledWorkspaceSync(workspace); - }); + test('deleteUntitledWorkspaceSync (saved)', async () => { + const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + service.deleteUntitledWorkspaceSync(workspace); }); - test('getUntitledWorkspaceSync', () => { + test('getUntitledWorkspaceSync', async () => { let untitled = service.getUntitledWorkspacesSync(); assert.equal(untitled.length, 0); - return createWorkspace([process.cwd(), os.tmpdir()]).then(untitledOne => { - assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); - - untitled = service.getUntitledWorkspacesSync(); - - assert.equal(1, untitled.length); - assert.equal(untitledOne.id, untitled[0].workspace.id); - - return createWorkspace([os.tmpdir(), process.cwd()]).then(untitledTwo => { - assert.ok(fs.existsSync(untitledTwo.configPath.fsPath)); - - untitled = service.getUntitledWorkspacesSync(); - - if (untitled.length === 1) { - assert.fail('Unexpected workspaces count, contents:\n' + fs.readFileSync(untitledTwo.configPath.fsPath, 'utf8')); - } - - assert.equal(2, untitled.length); + const untitledOne = await createWorkspace([process.cwd(), os.tmpdir()]); + assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); + + untitled = service.getUntitledWorkspacesSync(); + assert.equal(1, untitled.length); + assert.equal(untitledOne.id, untitled[0].workspace.id); + + const untitledTwo = await createWorkspace([os.tmpdir(), process.cwd()]); + assert.ok(fs.existsSync(untitledTwo.configPath.fsPath)); + assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`); + const untitledHome = dirname(dirname(untitledTwo.configPath)); + const beforeGettingUntitledWorkspaces = fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8')); + untitled = service.getUntitledWorkspacesSync(); + assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`); + if (untitled.length === 1) { + assert.fail(`Unexpected workspaces count of 1 (expected 2), all workspaces:\n ${fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8'))}, before getUntitledWorkspacesSync: ${beforeGettingUntitledWorkspaces}`); + } + assert.equal(2, untitled.length); - service.deleteUntitledWorkspaceSync(untitledOne); - untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); + service.deleteUntitledWorkspaceSync(untitledOne); + untitled = service.getUntitledWorkspacesSync(); + assert.equal(1, untitled.length); - service.deleteUntitledWorkspaceSync(untitledTwo); - untitled = service.getUntitledWorkspacesSync(); - assert.equal(0, untitled.length); - }); - }); + service.deleteUntitledWorkspaceSync(untitledTwo); + untitled = service.getUntitledWorkspacesSync(); + assert.equal(0, untitled.length); }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 38ff2ab34d18c..e047c9c2d95fb 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5208,7 +5208,7 @@ declare module 'vscode' { */ export enum TaskScope { /** - * The task is a global task + * The task is a global task. Global tasks are currrently not supported. */ Global = 1, @@ -5237,7 +5237,7 @@ declare module 'vscode' { * Creates a new task. * * @param definition The task definition as defined in the taskDefinitions extension point. - * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. + * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. Global tasks are currently not supported. * @param name The task's name. Is presented in the user interface. * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. * @param execution The process or shell execution. @@ -5923,6 +5923,30 @@ declare module 'vscode' { * @param message Body of the message. */ postMessage(message: any): Thenable; + + /** + * Convert a uri for the local file system to one that can be used inside webviews. + * + * Webviews cannot directly load resoruces from the workspace or local file system using `file:` uris. The + * `asWebviewUri` function takes a local `file:` uri and converts it into a uri that can be used inside of + * a webview to load the same resource: + * + * ```ts + * webview.html = `` + * ``` + */ + asWebviewUri(localResource: Uri): Uri; + + /** + * Content security policy source for webview resources. + * + * This is the origin that should be used in a content security policy rule: + * + * ``` + * img-src https: ${webview.cspSource} ...; + * ``` + */ + readonly cspSource: string; } /** @@ -6858,6 +6882,13 @@ declare module 'vscode' { * Whether to show collapse all action or not. */ showCollapseAll?: boolean; + + /** + * Whether the tree supports multi-select. When the tree supports multi-select and a command is executed from the tree, + * the first argument to the command is the tree item that the command was executed on and the second argument is an + * array containing the other selected tree items. + */ + canSelectMany?: boolean; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6328f593b3f41..4a15411821b46 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -565,22 +565,6 @@ declare module 'vscode' { //#endregion - //#region Joh: onDidExecuteCommand - - export interface CommandExecutionEvent { - command: string; - arguments: any[]; - } - - export namespace commands { - /** - * An event that is emitted when a [command](#Command) is executed. - */ - export const onDidExecuteCommand: Event; - } - - //#endregion - //#region Joh: decorations //todo@joh -> make class @@ -897,6 +881,11 @@ declare module 'vscode' { /** * An event that when fired will signal that the pty is closed and dispose of the terminal. * + * A number can be used to provide an exit code for the terminal. Exit codes must be + * positive and a non-zero exit codes signals failure which shows a notification for a + * regular terminal and allows dependent tasks to proceed when used with the + * `CustomExecution2` API. + * * **Example:** Exit the terminal when "y" is pressed, otherwise show a notification. * ```typescript * const writeEmitter = new vscode.EventEmitter(); @@ -915,7 +904,7 @@ declare module 'vscode' { * }; * vscode.window.createTerminal({ name: 'Exit example', pty }); */ - onDidClose?: Event; + onDidClose?: Event; /** * Implement to handle when the pty is open and ready to start firing events. @@ -1149,34 +1138,83 @@ declare module 'vscode' { //#endregion - //#region Webview Resource Roots + //#region Joh - CompletionItemTag, https://github.com/microsoft/vscode/issues/23927 + + export enum SymbolTag { + Deprecated = 1 + } - export interface Webview { + export interface SymbolInformation { /** - * Convert a uri for the local file system to one that can be used inside webviews. * - * Webviews cannot directly load resoruces from the workspace or local file system using `file:` uris. The - * `toWebviewResource` function takes a local `file:` uri and converts it into a uri that can be used inside of - * a webview to load the same resource: + */ + tags?: ReadonlyArray; + } + + export interface DocumentSymbol { + /** + * + */ + tags?: ReadonlyArray; + } + + export enum CompletionItemTag { + Deprecated = 1 + } + + export interface CompletionItem { + + /** * - * ```ts - * webview.html = `` - * ``` */ - toWebviewResource(localResource: Uri): Uri; + tags?: ReadonlyArray; + } + + //#endregion + + // #region Ben - extension auth flow (desktop+web) + + export interface AppUriOptions { + payload?: { + path?: string; + query?: string; + fragment?: string; + }; + } + + export namespace env { /** - * Content security policy source for webview resources. + * Creates a Uri that - if opened in a browser - will result in a + * registered [UriHandler](#UriHandler) to fire. The handler's + * Uri will be configured with the path, query and fragment of + * [AppUriOptions](#AppUriOptions) if provided, otherwise it will be empty. + * + * Extensions should not make any assumptions about the resulting + * Uri and should not alter it in anyway. Rather, extensions can e.g. + * use this Uri in an authentication flow, by adding the Uri as + * callback query argument to the server to authenticate to. * - * This is origin used in a content security policy rule: + * Note: If the server decides to add additional query parameters to the Uri + * (e.g. a token or secret), it will appear in the Uri that is passed + * to the [UriHandler](#UriHandler). * + * **Example** of an authentication flow: + * ```typescript + * vscode.window.registerUriHandler({ + * handleUri(uri: vscode.Uri): vscode.ProviderResult { + * if (uri.path === '/did-authenticate') { + * console.log(uri.toString()); + * } + * } + * }); + * + * const callableUri = await vscode.env.createAppUri({ payload: { path: '/did-authenticate' } }); + * await vscode.env.openExternal(callableUri); * ``` - * img-src https: ${webview.cspSource} ...; - * ```` */ - readonly cspSource: string; + export function createAppUri(options?: AppUriOptions): Thenable; } //#endregion - } diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 1e23e14ce69ca..053217cda5205 100644 --- a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts @@ -8,7 +8,7 @@ import * as modes from 'vs/editor/common/modes'; import { MainContext, MainThreadEditorInsetsShape, IExtHostContext, ExtHostEditorInsetsShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from '../common/extHostCustomers'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, IViewZone } from 'vs/editor/browser/editorBrowser'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -21,7 +21,7 @@ class EditorWebviewZone implements IViewZone { readonly afterColumn: number; readonly heightInLines: number; - private _id?: number; + private _id?: string; // suppressMouseDown?: boolean | undefined; // heightInPx?: number | undefined; // minWidthInPx?: number | undefined; @@ -90,7 +90,6 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape { const webview = this._webviewService.createWebview('' + handle, { enableFindWidget: false, - allowSvgs: false, extension: { id: extensionId, location: URI.revive(extensionLocation) } }, { allowScripts: options.enableScripts, diff --git a/src/vs/workbench/api/browser/mainThreadCommands.ts b/src/vs/workbench/api/browser/mainThreadCommands.ts index 25dc11380552e..2f4d043f04875 100644 --- a/src/vs/workbench/api/browser/mainThreadCommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCommands.ts @@ -15,7 +15,6 @@ export class MainThreadCommands implements MainThreadCommandsShape { private readonly _commandRegistrations = new Map(); private readonly _generateCommandsDocumentationRegistration: IDisposable; private readonly _proxy: ExtHostCommandsShape; - private _onDidExecuteCommandListener?: IDisposable; constructor( extHostContext: IExtHostContext, @@ -78,19 +77,6 @@ export class MainThreadCommands implements MainThreadCommandsShape { return this._commandService.executeCommand(id, ...args); } - $registerCommandListener() { - if (!this._onDidExecuteCommandListener) { - this._onDidExecuteCommandListener = this._commandService.onDidExecuteCommand(command => this._proxy.$handleDidExecuteCommand(command)); - } - } - - $unregisterCommandListener() { - if (this._onDidExecuteCommandListener) { - this._onDidExecuteCommandListener.dispose(); - this._onDidExecuteCommandListener = undefined; - } - } - $getCommands(): Promise { return Promise.resolve([...CommandsRegistry.getCommands().keys()]); } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 20572fa77839c..2f0f5283bfd21 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,10 +5,10 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI as uri } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, - IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto + IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import severity from 'vs/base/common/severity'; @@ -110,15 +110,16 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb // send all breakpoints const bps = this.debugService.getModel().getBreakpoints(); const fbps = this.debugService.getModel().getFunctionBreakpoints(); + const dbps = this.debugService.getModel().getDataBreakpoints(); if (bps.length > 0 || fbps.length > 0) { this._proxy.$acceptBreakpointsDelta({ - added: this.convertToDto(bps).concat(this.convertToDto(fbps)) + added: this.convertToDto(bps).concat(this.convertToDto(fbps)).concat(this.convertToDto(dbps)) }); } } } - public $registerBreakpoints(DTOs: Array): Promise { + public $registerBreakpoints(DTOs: Array): Promise { for (let dto of DTOs) { if (dto.type === 'sourceMulti') { @@ -136,14 +137,17 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps, 'extension'); } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id); + } else if (dto.type === 'data') { + this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist); } } return Promise.resolve(); } - public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): Promise { + public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise { breakpointIds.forEach(id => this.debugService.removeBreakpoints(id)); functionBreakpointIds.forEach(id => this.debugService.removeFunctionBreakpoints(id)); + dataBreakpointIds.forEach(id => this.debugService.removeDataBreakpoints(id)); return Promise.resolve(); } @@ -294,7 +298,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return undefined; } - private convertToDto(bps: (ReadonlyArray)): Array { + private convertToDto(bps: (ReadonlyArray)): Array { return bps.map(bp => { if ('name' in bp) { const fbp = bp; @@ -307,6 +311,19 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb logMessage: fbp.logMessage, functionName: fbp.name }; + } else if ('dataId' in bp) { + const dbp = bp; + return { + type: 'data', + id: dbp.getId(), + dataId: dbp.dataId, + enabled: dbp.enabled, + condition: dbp.condition, + hitCondition: dbp.hitCondition, + logMessage: dbp.logMessage, + label: dbp.label, + canPersist: dbp.canPersist + }; } else { const sbp = bp; return { diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index 801a17d89a703..eb26b62da3aa2 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -5,24 +5,43 @@ import { IMarkerService, IMarkerData } from 'vs/platform/markers/common/markers'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { MainThreadDiagnosticsShape, MainContext, IExtHostContext } from '../common/extHost.protocol'; +import { MainThreadDiagnosticsShape, MainContext, IExtHostContext, ExtHostDiagnosticsShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { IDisposable } from 'vs/base/common/lifecycle'; @extHostNamedCustomer(MainContext.MainThreadDiagnostics) export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { private readonly _activeOwners = new Set(); + + private readonly _proxy: ExtHostDiagnosticsShape; private readonly _markerService: IMarkerService; + private readonly _markerListener: IDisposable; constructor( extHostContext: IExtHostContext, @IMarkerService markerService: IMarkerService ) { + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDiagnostics); this._markerService = markerService; + this._markerListener = this._markerService.onMarkerChanged(this._forwardMarkers, this); } dispose(): void { + this._markerListener.dispose(); this._activeOwners.forEach(owner => this._markerService.changeAll(owner, [])); + this._activeOwners.clear(); + } + + private _forwardMarkers(resources: URI[]): void { + const data: [UriComponents, IMarkerData[]][] = []; + for (const resource of resources) { + data.push([ + resource, + this._markerService.read({ resource }).filter(marker => !this._activeOwners.has(marker.owner)) + ]); + } + this._proxy.$acceptMarkersChange(data); } $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { diff --git a/src/vs/workbench/api/browser/mainThreadKeytar.ts b/src/vs/workbench/api/browser/mainThreadKeytar.ts index fff0a902e2c72..f07b7688801a3 100644 --- a/src/vs/workbench/api/browser/mainThreadKeytar.ts +++ b/src/vs/workbench/api/browser/mainThreadKeytar.ts @@ -5,49 +5,33 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadKeytarShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; @extHostNamedCustomer(MainContext.MainThreadKeytar) export class MainThreadKeytar implements MainThreadKeytarShape { - private readonly _credentialsService?: ICredentialsService; - constructor( _extHostContext: IExtHostContext, - @optional(ICredentialsService) credentialsService: ICredentialsService, - ) { - this._credentialsService = credentialsService; - } - - dispose(): void { - // - } + @ICredentialsService private readonly _credentialsService: ICredentialsService, + ) { } async $getPassword(service: string, account: string): Promise { - if (this._credentialsService) { - return this._credentialsService.getPassword(service, account); - } - return null; + return this._credentialsService.getPassword(service, account); } async $setPassword(service: string, account: string, password: string): Promise { - if (this._credentialsService) { - return this._credentialsService.setPassword(service, account, password); - } + return this._credentialsService.setPassword(service, account, password); } async $deletePassword(service: string, account: string): Promise { - if (this._credentialsService) { - return this._credentialsService.deletePassword(service, account); - } - return false; + return this._credentialsService.deletePassword(service, account); } async $findPassword(service: string): Promise { - if (this._credentialsService) { - return this._credentialsService.findPassword(service); - } - return null; + return this._credentialsService.findPassword(service); + } + + dispose(): void { + // } } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 5cc6f7fa4c90c..966a740a4f112 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -330,6 +330,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { label: data.a, kind: data.b, + tags: data.n, detail: data.c, documentation: data.d, sortText: data.e, @@ -365,7 +366,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }; if (supportsResolveDetails) { provider.resolveCompletionItem = (model, position, suggestion, token) => { - return this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion._id, token).then(result => { + return this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion._id!, token).then(result => { if (!result) { return suggestion; } diff --git a/src/vs/workbench/api/browser/mainThreadLogService.ts b/src/vs/workbench/api/browser/mainThreadLogService.ts index b19ef2e78b12a..f8eef4dd91cc0 100644 --- a/src/vs/workbench/api/browser/mainThreadLogService.ts +++ b/src/vs/workbench/api/browser/mainThreadLogService.ts @@ -3,20 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtHostContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IExtHostContext, ExtHostContext, MainThreadLogShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { UriComponents, URI } from 'vs/base/common/uri'; +import { FileLogService } from 'vs/platform/log/common/fileLogService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { basename } from 'vs/base/common/path'; -@extHostCustomer -export class MainThreadLogService extends Disposable { +@extHostNamedCustomer(MainContext.MainThreadLog) +export class MainThreadLogService implements MainThreadLogShape { + + private readonly _loggers = new Map(); + private readonly _logListener: IDisposable; constructor( extHostContext: IExtHostContext, - @ILogService logService: ILogService, + @ILogService private readonly _logService: ILogService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { - super(); - this._register(logService.onDidChangeLogLevel(level => extHostContext.getProxy(ExtHostContext.ExtHostLogService).$setLevel(level))); + const proxy = extHostContext.getProxy(ExtHostContext.ExtHostLogService); + this._logListener = _logService.onDidChangeLogLevel(level => { + proxy.$setLevel(level); + this._loggers.forEach(value => value.setLevel(level)); + }); + } + + dispose(): void { + this._logListener.dispose(); + this._loggers.forEach(value => value.dispose()); + this._loggers.clear(); } -} \ No newline at end of file + $log(file: UriComponents, level: LogLevel, message: any[]): void { + const uri = URI.revive(file); + let logger = this._loggers.get(uri.toString()); + if (!logger) { + logger = this._instaService.createInstance(FileLogService, basename(file.path), URI.revive(file), this._logService.getLevel()); + this._loggers.set(uri.toString(), logger); + } + logger.log(level, message); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 2a1e3f5bc5fdc..3c2e1c7080177 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -11,6 +11,7 @@ import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isUndefinedOrNull, isNumber } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -21,24 +22,28 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie constructor( extHostContext: IExtHostContext, @IViewsService private readonly viewsService: IViewsService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void { - const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); - this._dataProviders.set(treeViewId, dataProvider); - const viewer = this.getTreeView(treeViewId); - if (viewer) { - viewer.dataProvider = dataProvider; - viewer.showCollapseAllAction = !!options.showCollapseAll; - this.registerListeners(treeViewId, viewer); - this._proxy.$setVisible(treeViewId, viewer.visible); - } else { - this.notificationService.error('No view is registered with id: ' + treeViewId); - } + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): void { + this.extensionService.whenInstalledExtensionsRegistered().then(() => { + const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); + this._dataProviders.set(treeViewId, dataProvider); + const viewer = this.getTreeView(treeViewId); + if (viewer) { + viewer.dataProvider = dataProvider; + viewer.showCollapseAllAction = !!options.showCollapseAll; + viewer.canSelectMany = !!options.canSelectMany; + this.registerListeners(treeViewId, viewer); + this._proxy.$setVisible(treeViewId, viewer.visible); + } else { + this.notificationService.error('No view is registered with id: ' + treeViewId); + } + }); } $reveal(treeViewId: string, item: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise { @@ -124,7 +129,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._dataProviders.forEach((dataProvider, treeViewId) => { const treeView = this.getTreeView(treeViewId); if (treeView) { - treeView.dataProvider = null; + treeView.dataProvider = undefined; } }); this._dataProviders.clear(); diff --git a/src/vs/workbench/api/browser/mainThreadUrls.ts b/src/vs/workbench/api/browser/mainThreadUrls.ts index 224d51f978ccf..75fdde3e49530 100644 --- a/src/vs/workbench/api/browser/mainThreadUrls.ts +++ b/src/vs/workbench/api/browser/mainThreadUrls.ts @@ -6,7 +6,7 @@ import { ExtHostContext, IExtHostContext, MainContext, MainThreadUrlsShape, ExtHostUrlsShape } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from '../common/extHostCustomers'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IExtensionUrlHandler } from 'vs/workbench/services/extensions/common/inactiveExtensionUrlHandler'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -68,6 +68,16 @@ export class MainThreadUrls implements MainThreadUrlsShape { return Promise.resolve(undefined); } + async $createAppUri(extensionId: ExtensionIdentifier, options?: { payload?: Partial }): Promise { + const payload: Partial = options && options.payload ? options.payload : Object.create(null); + + // we define the authority to be the extension ID to ensure + // that the Uri gets routed back to the extension properly. + payload.authority = extensionId.value; + + return this.urlService.create(payload); + } + dispose(): void { this.handlers.forEach(({ disposable }) => disposable.dispose()); this.handlers.clear(); diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 90bdeb5ea748a..d657e9d0d96fd 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -5,30 +5,62 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import * as map from 'vs/base/common/map'; +import { startsWith } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions, WebviewPanelViewStateData } from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { extHostNamedCustomer } from '../common/extHostCustomers'; -import { IProductService } from 'vs/platform/product/common/product'; -import { startsWith } from 'vs/base/common/strings'; -import { Webview } from 'vs/workbench/contrib/webview/common/webview'; interface OldMainThreadWebviewState { readonly viewType: string; state: any; } +/** + * Bi-directional map between webview handles and inputs. + */ +class WebviewHandleStore { + private readonly _handlesToInputs = new Map(); + private readonly _inputsToHandles = new Map(); + + public add(handle: string, input: WebviewEditorInput): void { + this._handlesToInputs.set(handle, input); + this._inputsToHandles.set(input, handle); + } + + public getHandleForInput(input: WebviewEditorInput): string | undefined { + return this._inputsToHandles.get(input); + } + + public getInputForHandle(handle: string): WebviewEditorInput | undefined { + return this._handlesToInputs.get(handle); + } + + public delete(handle: string): void { + const input = this.getInputForHandle(handle); + this._handlesToInputs.delete(handle); + if (input) { + this._inputsToHandles.delete(input); + } + } + + public get size(): number { + return this._handlesToInputs.size; + } +} + @extHostNamedCustomer(MainContext.MainThreadWebviews) export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape { @@ -43,12 +75,9 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews private static revivalPool = 0; private readonly _proxy: ExtHostWebviewsShape; - private readonly _webviewEditorInputs = new Map(); - private readonly _webviews = new Map(); + private readonly _webviewEditorInputs = new WebviewHandleStore(); private readonly _revivers = new Map(); - private _activeWebview: WebviewPanelHandle | undefined = undefined; - constructor( context: IExtHostContext, @IExtensionService extensionService: IExtensionService, @@ -62,8 +91,8 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews super(); this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); - this._register(_editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this)); - this._register(_editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this)); + this._register(_editorService.onDidActiveEditorChange(this.updateWebviewViewStates, this)); + this._register(_editorService.onDidVisibleEditorsChange(this.updateWebviewViewStates, this)); // This reviver's only job is to activate webview extensions // This should trigger the real reviver to be registered from the extension host side. @@ -104,8 +133,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews }); this.hookupWebviewEventDelegate(handle, webview); - this._webviewEditorInputs.set(handle, webview); - this._webviews.set(handle, webview.webview); + this._webviewEditorInputs.add(handle, webview); /* __GDPR__ "webviews:createWebviewPanel" : { @@ -175,8 +203,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } const handle = `revival-${MainThreadWebviews.revivalPool++}`; - this._webviewEditorInputs.set(handle, webviewEditorInput); - this._webviews.set(handle, webviewEditorInput.webview); + this._webviewEditorInputs.add(handle, webviewEditorInput); this.hookupWebviewEventDelegate(handle, webviewEditorInput); let state = undefined; @@ -234,7 +261,6 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews input.onDispose(() => { this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { this._webviewEditorInputs.delete(handle); - this._webviews.delete(handle); }); }); input.webview.onDidUpdateState((newState: any) => { @@ -246,77 +272,38 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews }); } - private onActiveEditorChanged() { - const activeEditor = this._editorService.activeControl; - let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined; - if (activeEditor && activeEditor.input instanceof WebviewEditorInput) { - for (const handle of map.keys(this._webviewEditorInputs)) { - const input = this._webviewEditorInputs.get(handle)!; - if (input.matches(activeEditor.input)) { - newActiveWebview = { input, handle }; - break; - } - } - } - - if (newActiveWebview && newActiveWebview.handle === this._activeWebview) { - // Webview itself unchanged but position may have changed - this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, { - active: true, - visible: true, - position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group || 0) - }); + private updateWebviewViewStates() { + if (!this._webviewEditorInputs.size) { return; } - // Broadcast view state update for currently active - if (typeof this._activeWebview !== 'undefined') { - const oldActiveWebview = this._webviewEditorInputs.get(this._activeWebview); - if (oldActiveWebview) { - this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, { - active: false, - visible: this._editorService.visibleControls.some(editor => !!editor.input && editor.input.matches(oldActiveWebview)), - position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group || 0), - }); - } - } + const activeInput = this._editorService.activeControl && this._editorService.activeControl.input; + const viewStates: WebviewPanelViewStateData = {}; + for (const group of this._editorGroupService.groups) { + for (const input of group.editors) { + if (!(input instanceof WebviewEditorInput)) { + continue; + } - // Then for newly active - if (newActiveWebview) { - this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, { - active: true, - visible: true, - position: editorGroupToViewColumn(this._editorGroupService, activeEditor ? activeEditor.group : ACTIVE_GROUP), - }); - this._activeWebview = newActiveWebview.handle; - } else { - this._activeWebview = undefined; - } - } + input.updateGroup(group.id); - private onVisibleEditorsChanged(): void { - this._webviewEditorInputs.forEach((input, handle) => { - for (const workbenchEditor of this._editorService.visibleControls) { - if (workbenchEditor.input && workbenchEditor.input.matches(input)) { - const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group!); - - input.updateGroup(workbenchEditor.group!.id); - this._proxy.$onDidChangeWebviewPanelViewState(handle, { - active: handle === this._activeWebview, - visible: true, - position: editorPosition - }); - break; + const handle = this._webviewEditorInputs.getHandleForInput(input); + if (handle) { + viewStates[handle] = { + visible: input === group.activeEditor, + active: input === activeInput, + position: editorGroupToViewColumn(this._editorGroupService, group.id), + }; } } - }); - } + } - private onDidClickLink(handle: WebviewPanelHandle, link: URI): void { - if (!link) { - return; + if (Object.keys(viewStates).length) { + this._proxy.$onDidChangeWebviewPanelViewStates(viewStates); } + } + private onDidClickLink(handle: WebviewPanelHandle, link: URI): void { const webview = this.getWebviewEditorInput(handle); if (this.isSupportedLink(webview, link)) { this._openerService.open(link); @@ -342,28 +329,19 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } private tryGetWebviewEditorInput(handle: WebviewPanelHandle): WebviewEditorInput | undefined { - return this._webviewEditorInputs.get(handle); + return this._webviewEditorInputs.getInputForHandle(handle); } private getWebview(handle: WebviewPanelHandle): Webview { - const webview = this.tryGetWebview(handle); - if (!webview) { - throw new Error('Unknown webview handle:' + handle); - } - return webview; - } - - private tryGetWebview(handle: WebviewPanelHandle): Webview | undefined { - return this._webviews.get(handle); + return this.getWebviewEditorInput(handle).webview; } private static getDeserializationFailedContents(viewType: string) { return ` - - + ${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)} `; diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 410bb4ec66d30..df059758f65e9 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -6,12 +6,13 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowService } from 'vs/platform/windows/common/windows'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostWindowShape, IExtHostContext, MainContext, MainThreadWindowShape, IOpenUriOptions } from '../common/extHost.protocol'; import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { extractLocalHostUriMetaDataForPortMapping } from 'vs/workbench/contrib/webview/common/portMapping'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; @extHostNamedCustomer(MainContext.MainThreadWindow) export class MainThreadWindow implements MainThreadWindowShape { @@ -23,7 +24,7 @@ export class MainThreadWindow implements MainThreadWindowShape { constructor( extHostContext: IExtHostContext, @IWindowService private readonly windowService: IWindowService, - @IWindowsService private readonly windowsService: IWindowsService, + @IOpenerService private readonly openerService: IOpenerService, @ITunnelService private readonly tunnelService: ITunnelService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { @@ -58,7 +59,7 @@ export class MainThreadWindow implements MainThreadWindowShape { } } - return this.windowsService.openExternal(encodeURI(uri.toString(true))); + return this.openerService.open(uri, { openExternal: true }); } private getOrCreateTunnel(remotePort: number): Promise | undefined { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 149f1f4cebecf..e6621fc362cae 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -19,6 +19,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; +import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/common/remote.contribution'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from 'vs/workbench/browser/viewlet'; @@ -36,7 +37,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { createCSSRule, asDomUri } from 'vs/base/browser/dom'; +import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -79,6 +80,7 @@ interface IUserFriendlyViewDescriptor { id: string; name: string; when?: string; + group?: string; } const viewDescriptor: IJSONSchema = { @@ -99,6 +101,27 @@ const viewDescriptor: IJSONSchema = { } }; +const nestableViewDescriptor: IJSONSchema = { + type: 'object', + properties: { + id: { + description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'), + type: 'string' + }, + name: { + description: localize('vscode.extension.contributes.view.name', 'The human-readable name of the view. Will be shown'), + type: 'string' + }, + when: { + description: localize('vscode.extension.contributes.view.when', 'Condition which must be true to show this view'), + type: 'string' + }, + group: { + description: localize('vscode.extension.contributes.view.group', 'Nested group in the viewlet'), + type: 'string' + } + } +}; const viewsContribution: IJSONSchema = { description: localize('vscode.extension.contributes.views', "Contributes views to the editor"), type: 'object', @@ -126,6 +149,12 @@ const viewsContribution: IJSONSchema = { type: 'array', items: viewDescriptor, default: [] + }, + 'remote': { + description: localize('views.remote', "Contributes views to Remote container in the Activity bar. To contribute to this container, enableProposedApi needs to be turned on"), + type: 'array', + items: nestableViewDescriptor, + default: [] } }, additionalProperties: { @@ -181,7 +210,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { }); } - private addCustomViewContainers(extensionPoints: IExtensionPointUser[], existingViewContainers: ViewContainer[]): void { + private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser[], existingViewContainers: ViewContainer[]): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); let order = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId).length + 1; for (let { value, collector, description } of extensionPoints) { @@ -198,7 +227,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } } - private removeCustomViewContainers(extensionPoints: IExtensionPointUser[]): void { + private removeCustomViewContainers(extensionPoints: readonly IExtensionPointUser[]): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const removedExtensions: Set = extensionPoints.reduce((result, e) => { result.add(ExtensionIdentifier.toKey(e.description.identifier)); return result; }, new Set()); for (const viewContainer of viewContainersRegistry.all) { @@ -327,7 +356,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { // Generate CSS to show the icon in the activity bar const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; - createCSSRule(iconClass, `-webkit-mask: url('${asDomUri(icon)}') no-repeat 50% 50%; -webkit-mask-size: 24px;`); + createCSSRule(iconClass, `-webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; -webkit-mask-size: 24px;`); } return viewContainer; @@ -349,7 +378,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { }); } - private addViews(extensions: IExtensionPointUser[]): void { + private addViews(extensions: readonly IExtensionPointUser[]): void { for (const extension of extensions) { const { value, collector } = extension; @@ -358,6 +387,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return; } + if (entry.key === 'remote' && !extension.description.enableProposedApi) { + collector.warn(localize('ViewContainerRequiresProposedAPI', "View container '{0}' requires 'enableProposedApi' turned on to be added to 'Remote'.", entry.key)); + return; + } + const viewContainer = this.getViewContainer(entry.key); if (!viewContainer) { collector.warn(localize('ViewContainerDoesnotExist', "View container '{0}' does not exist and all views registered to it will be added to 'Explorer'.", entry.key)); @@ -376,6 +410,12 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return null; } + const order = ExtensionIdentifier.equals(extension.description.identifier, container.extensionId) + ? index + 1 + : container.orderDelegate + ? container.orderDelegate.getOrder(item.group) + : undefined; + const viewDescriptor = { id: item.id, name: item.name, @@ -384,9 +424,10 @@ class ViewsExtensionHandler implements IWorkbenchContribution { canToggleVisibility: true, collapsed: this.showCollapsed(container), treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name, container), - order: ExtensionIdentifier.equals(extension.description.identifier, container.extensionId) ? index + 1 : undefined, + order: order, extensionId: extension.description.identifier, - originalContainerId: entry.key + originalContainerId: entry.key, + group: item.group }; viewIds.push(viewDescriptor.id); @@ -401,7 +442,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return this.viewContainersRegistry.get(EXPLORER)!; } - private removeViews(extensions: IExtensionPointUser[]): void { + private removeViews(extensions: readonly IExtensionPointUser[]): void { const removedExtensions: Set = extensions.reduce((result, e) => { result.add(ExtensionIdentifier.toKey(e.description.identifier)); return result; }, new Set()); for (const viewContainer of this.viewContainersRegistry.all) { const removedViews = this.viewsRegistry.getViews(viewContainer).filter((v: ICustomViewDescriptor) => v.extensionId && removedExtensions.has(ExtensionIdentifier.toKey(v.extensionId))); @@ -440,6 +481,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { case 'explorer': return this.viewContainersRegistry.get(EXPLORER); case 'debug': return this.viewContainersRegistry.get(DEBUG); case 'scm': return this.viewContainersRegistry.get(SCM); + case 'remote': return this.viewContainersRegistry.get(REMOTE); default: return this.viewContainersRegistry.get(`workbench.view.extension.${value}`); } } diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 0c5b098cd6aff..28bbc68d9bfc5 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -175,6 +175,11 @@ CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async functio return windowService.addRecentlyOpened([recent]); }); +CommandsRegistry.registerCommand('_workbench.getRecentlyOpened', async function (accessor: ServicesAccessor) { + const windowService = accessor.get(IWindowService); + return windowService.getRecentlyOpened(); +}); + export class SetEditorLayoutAPICommand { public static ID = 'vscode.setEditorLayout'; public static execute(executor: ICommandsExecutor, layout: EditorGroupLayout): Promise { @@ -205,4 +210,4 @@ CommandsRegistry.registerCommand({ } }] } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index aa05f7e1187e2..d36d498330dd3 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -39,13 +39,14 @@ const configurationEntrySchema: IJSONSchema = { }, scope: { type: 'string', - enum: ['application', 'machine', 'window', 'resource'], + enum: ['application', 'machine', 'window', 'resource', 'machine-overridable'], default: 'window', enumDescriptions: [ - nls.localize('scope.application.description', "Application specific configuration, which can be configured only in the user settings."), - nls.localize('scope.machine.description', "Machine specific configuration, which can be configured only in the user settings when the extension is running locally, or only in the remote settings when the extension is running remotely."), - nls.localize('scope.window.description', "Window specific configuration, which can be configured in the user, remote or workspace settings."), - nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the user, remote, workspace or folder settings.") + nls.localize('scope.application.description', "Configuration that can be configured only in the user settings."), + nls.localize('scope.machine.description', "Configuration that can be configured only in the user settings when the extension is running locally, or only in the remote settings when the extension is running remotely."), + nls.localize('scope.window.description', "Configuration that can be configured in the user, remote or workspace settings."), + nls.localize('scope.resource.description', "Configuration that can be configured in the user, remote, workspace or folder settings."), + nls.localize('scope.machine-overridable.description', "Machine configuration that can be configured also in workspace or folder settings.") ], description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `application`, `machine`, `window` and `resource`.") }, @@ -215,6 +216,8 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten propertyConfiguration.scope = ConfigurationScope.MACHINE; } else if (propertyConfiguration.scope.toString() === 'resource') { propertyConfiguration.scope = ConfigurationScope.RESOURCE; + } else if (propertyConfiguration.scope.toString() === 'machine-overridable') { + propertyConfiguration.scope = ConfigurationScope.MACHINE_OVERRIDABLE; } else { propertyConfiguration.scope = ConfigurationScope.WINDOW; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e8fec87502911..c5fd74ffe75f4 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -15,7 +15,7 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import * as languageConfiguration from 'vs/editor/common/modes/languageConfiguration'; import { score } from 'vs/editor/common/modes/languageSelector'; import * as files from 'vs/platform/files/common/files'; -import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, MainContext, ExtHostLogServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -33,7 +33,6 @@ import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; -import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { ExtHostMessageService } from 'vs/workbench/api/common/extHostMessageService'; import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; import { ExtHostProgress } from 'vs/workbench/api/common/extHostProgress'; @@ -74,14 +73,6 @@ export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; } -function proposedApiFunction(extension: IExtensionDescription, fn: T): T { - if (extension.enableProposedApi) { - return fn; - } else { - return throwProposedApiError.bind(null, extension) as any as T; - } -} - /** * This method instantiates and returns the extension API surface */ @@ -95,10 +86,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const uriTransformer = accessor.get(IURITransformerService); const rpcProtocol = accessor.get(IExtHostRpcService); const extHostStorage = accessor.get(IExtHostStorage); - const extHostLogService = accessor.get(ILogService); + const extHostLogService = accessor.get(ILogService); // register addressable instances - rpcProtocol.set(ExtHostContext.ExtHostLogService, extHostLogService); + rpcProtocol.set(ExtHostContext.ExtHostLogService, extHostLogService); rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace); rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); @@ -145,10 +136,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStatusBar = new ExtHostStatusBar(rpcProtocol); const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments); - // Register an output channel for exthost log - const outputChannelName = initData.remote.isRemote ? nls.localize('remote extension host Log', "Remote Extension Host") : nls.localize('extension host Log', "Extension Host"); - extHostOutputService.createOutputChannelFromLogFile(outputChannelName, extHostLogService.logFile); - // Register API-ish commands ExtHostApiCommands.register(extHostCommands); @@ -211,7 +198,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }); }); }, - registerDiffInformationCommand: proposedApiFunction(extension, (id: string, callback: (diff: vscode.LineChange[], ...args: any[]) => any, thisArg?: any): vscode.Disposable => { + registerDiffInformationCommand: (id: string, callback: (diff: vscode.LineChange[], ...args: any[]) => any, thisArg?: any): vscode.Disposable => { + checkProposedApiEnabled(extension); return extHostCommands.registerCommand(true, id, async (...args: any[]): Promise => { const activeTextEditor = extHostEditors.getActiveTextEditor(); if (!activeTextEditor) { @@ -222,17 +210,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const diff = await extHostEditors.getDiffInformation(activeTextEditor.id); callback.apply(thisArg, [diff, ...args]); }); - }), + }, executeCommand(id: string, ...args: any[]): Thenable { return extHostCommands.executeCommand(id, ...args); }, getCommands(filterInternal: boolean = false): Thenable { return extHostCommands.getCommands(filterInternal); - }, - onDidExecuteCommand: proposedApiFunction(extension, (listener, thisArgs?, disposables?) => { - checkProposedApiEnabled(extension); - return extHostCommands.onDidExecuteCommand(listener, thisArgs, disposables); - }), + } }; // namespace: env @@ -243,6 +227,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get appName() { return initData.environment.appName; }, get appRoot() { return initData.environment.appRoot!.fsPath; }, get uriScheme() { return initData.environment.appUriScheme; }, + createAppUri(options?) { + checkProposedApiEnabled(extension); + return extHostUrls.createAppUri(extension.identifier, options); + }, get logLevel() { checkProposedApiEnabled(extension); return typeConverters.LogLevel.to(extHostLogService.getLevel()); @@ -535,9 +523,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); }, - registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { + registerDecorationProvider(provider: vscode.DecorationProvider) { + checkProposedApiEnabled(extension); return extHostDecorations.registerDecorationProvider(provider, extension.identifier); - }), + }, registerUriHandler(handler: vscode.UriHandler) { return extHostUrls.registerUriHandler(extension.identifier, handler); }, @@ -550,8 +539,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: workspace + let warnedRootPathDeprecated = false; const workspace: typeof vscode.workspace = { get rootPath() { + if (extension.isUnderDevelopment && !warnedRootPathDeprecated) { + warnedRootPathDeprecated = true; + console.warn(`[Deprecation Warning] 'workspace.rootPath' is deprecated and should no longer be used. Please use 'workspace.workspaceFolders' instead. More details: https://aka.ms/vscode-eliminating-rootpath`); + } + return extHostWorkspace.getPath(); }, set rootPath(value) { @@ -672,24 +667,30 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get fs() { return extHostFileSystem.fileSystem; }, - registerFileSearchProvider: proposedApiFunction(extension, (scheme: string, provider: vscode.FileSearchProvider) => { + registerFileSearchProvider: (scheme: string, provider: vscode.FileSearchProvider) => { + checkProposedApiEnabled(extension); return extHostSearch.registerFileSearchProvider(scheme, provider); - }), - registerTextSearchProvider: proposedApiFunction(extension, (scheme: string, provider: vscode.TextSearchProvider) => { + }, + registerTextSearchProvider: (scheme: string, provider: vscode.TextSearchProvider) => { + checkProposedApiEnabled(extension); return extHostSearch.registerTextSearchProvider(scheme, provider); - }), - registerRemoteAuthorityResolver: proposedApiFunction(extension, (authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver) => { + }, + registerRemoteAuthorityResolver: (authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver) => { + checkProposedApiEnabled(extension); return extensionService.registerRemoteAuthorityResolver(authorityPrefix, resolver); - }), - registerResourceLabelFormatter: proposedApiFunction(extension, (formatter: vscode.ResourceLabelFormatter) => { + }, + registerResourceLabelFormatter: (formatter: vscode.ResourceLabelFormatter) => { + checkProposedApiEnabled(extension); return extHostLabelService.$registerResourceLabelFormatter(formatter); - }), - onDidRenameFile: proposedApiFunction(extension, (listener: (e: vscode.FileRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + }, + onDidRenameFile: (listener: (e: vscode.FileRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + checkProposedApiEnabled(extension); return extHostFileSystemEvent.onDidRenameFile(listener, thisArg, disposables); - }), - onWillRenameFile: proposedApiFunction(extension, (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + }, + onWillRenameFile: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + checkProposedApiEnabled(extension); return extHostFileSystemEvent.getOnWillRenameFileEvent(extension)(listener, thisArg, disposables); - }) + } }; // namespace: scm @@ -812,6 +813,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CommentMode: extHostTypes.CommentMode, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, + CompletionItemTag: extHostTypes.CompletionItemTag, CompletionList: extHostTypes.CompletionList, CompletionTriggerKind: extHostTypes.CompletionTriggerKind, ConfigurationTarget: extHostTypes.ConfigurationTarget, @@ -866,6 +868,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I StatusBarAlignment: extHostTypes.StatusBarAlignment, SymbolInformation: extHostTypes.SymbolInformation, SymbolKind: extHostTypes.SymbolKind, + SymbolTag: extHostTypes.SymbolTag, Task: extHostTypes.Task, Task2: extHostTypes.Task, TaskGroup: extHostTypes.TaskGroup, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e4a618d695bc2..01955cf7dfad9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -21,7 +21,7 @@ import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model' import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import * as modes from 'vs/editor/common/modes'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; -import { ICommandHandlerDescription, ICommandEvent } from 'vs/platform/commands/common/commands'; +import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationData, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -115,8 +115,6 @@ export interface MainThreadClipboardShape extends IDisposable { export interface MainThreadCommandsShape extends IDisposable { $registerCommand(id: string): void; - $registerCommandListener(): void; - $unregisterCommandListener(): void; $unregisterCommand(id: string): void; $executeCommand(id: string, args: any[]): Promise; $getCommands(): Promise; @@ -242,7 +240,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): void; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise; $reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise; $setMessage(treeViewId: string, message: string): void; @@ -551,15 +549,17 @@ export interface MainThreadWebviewsShape extends IDisposable { $unregisterSerializer(viewType: string): void; } -export interface WebviewPanelViewState { - readonly active: boolean; - readonly visible: boolean; - readonly position: EditorViewColumn; +export interface WebviewPanelViewStateData { + [handle: string]: { + readonly active: boolean; + readonly visible: boolean; + readonly position: EditorViewColumn; + }; } export interface ExtHostWebviewsShape { $onMessage(handle: WebviewPanelHandle, message: any): void; - $onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, newState: WebviewPanelViewState): void; + $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; } @@ -567,6 +567,7 @@ export interface ExtHostWebviewsShape { export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; + $createAppUri(extensionId: ExtensionIdentifier, options?: { payload?: Partial }): Promise; } export interface ExtHostUrlsShape { @@ -717,8 +718,8 @@ export interface MainThreadDebugServiceShape extends IDisposable { $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; $appendDebugConsole(value: string): void; $startBreakpointEvents(): void; - $registerBreakpoints(breakpoints: Array): Promise; - $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): Promise; + $registerBreakpoints(breakpoints: Array): Promise; + $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise; } export interface IOpenUriOptions { @@ -735,7 +736,6 @@ export interface MainThreadWindowShape extends IDisposable { export interface ExtHostCommandsShape { $executeContributedCommand(id: string, ...args: any[]): Promise; $getContributedCommandHandlerDescriptions(): Promise<{ [id: string]: string | ICommandHandlerDescription }>; - $handleDidExecuteCommand(command: ICommandEvent): void; } export interface ExtHostConfigurationShape { @@ -744,7 +744,7 @@ export interface ExtHostConfigurationShape { } export interface ExtHostDiagnosticsShape { - + $acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void; } export interface ExtHostDocumentContentProvidersShape { @@ -935,6 +935,7 @@ export interface ISuggestDataDto { k/* commitCharacters */?: string[]; l/* additionalTextEdits */?: ISingleEditOperation[]; m/* command */?: modes.Command; + n/* kindModifier */?: modes.CompletionItemTag[]; // not-standard x?: ChainedCacheId; } @@ -1201,6 +1202,13 @@ export interface IFunctionBreakpointDto extends IBreakpointDto { functionName: string; } +export interface IDataBreakpointDto extends IBreakpointDto { + type: 'data'; + dataId: string; + canPersist: boolean; + label: string; +} + export interface ISourceBreakpointDto extends IBreakpointDto { type: 'source'; uri: UriComponents; @@ -1209,9 +1217,9 @@ export interface ISourceBreakpointDto extends IBreakpointDto { } export interface IBreakpointsDeltaDto { - added?: Array; + added?: Array; removed?: string[]; - changed?: Array; + changed?: Array; } export interface ISourceMultiBreakpointDto { @@ -1277,6 +1285,10 @@ export interface ExtHostLogServiceShape { $setLevel(level: LogLevel): void; } +export interface MainThreadLogShape { + $log(file: UriComponents, level: LogLevel, args: any[]): void; +} + export interface ExtHostOutputServiceShape { $setVisibleChannel(channelId: string | null): void; } @@ -1319,6 +1331,7 @@ export const MainContext = { MainThreadKeytar: createMainId('MainThreadKeytar'), MainThreadLanguageFeatures: createMainId('MainThreadLanguageFeatures'), MainThreadLanguages: createMainId('MainThreadLanguages'), + MainThreadLog: createMainId('MainThread'), MainThreadMessageService: createMainId('MainThreadMessageService'), MainThreadOutputService: createMainId('MainThreadOutputService'), MainThreadProgress: createMainId('MainThreadProgress'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index ff2a106f427f2..d84da000c3fdd 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -135,7 +135,7 @@ export class ExtHostApiCommands { description: 'Execute code action provider.', args: [ { name: 'uri', description: 'Uri of a text document', constraint: URI }, - { name: 'range', description: 'Range in a text document', constraint: types.Range }, + { name: 'rangeOrSelection', description: 'Range in a text document. Some refactoring provider requires Selection object.', constraint: types.Range }, { name: 'kind', description: '(optional) Code action kind to return code actions for', constraint: (value: any) => !value || typeof value.value === 'string' }, ], returns: 'A promise that resolves to an array of Command-instances.' @@ -480,10 +480,12 @@ export class ExtHostApiCommands { }); } - private _executeCodeActionProvider(resource: URI, range: types.Range, kind?: string): Promise<(vscode.CodeAction | vscode.Command | undefined)[] | undefined> { + private _executeCodeActionProvider(resource: URI, rangeOrSelection: types.Range | types.Selection, kind?: string): Promise<(vscode.CodeAction | vscode.Command | undefined)[] | undefined> { const args = { resource, - range: typeConverters.Range.from(range), + rangeOrSelection: types.Selection.isSelection(rangeOrSelection) + ? typeConverters.Selection.from(rangeOrSelection) + : typeConverters.Range.from(rangeOrSelection), kind }; return this._commands.executeCommand('_executeCodeActionProvider', args) @@ -504,6 +506,7 @@ export class ExtHostApiCommands { if (codeAction.command) { ret.command = this._commands.converter.fromInternal(codeAction.command); } + ret.isPreferred = codeAction.isPreferred; return ret; } })); diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 066b25f2b026d..769e4de2a7a06 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -10,7 +10,7 @@ import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; -import { toWebviewResource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -65,8 +65,8 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _html: string = ''; private _options: vscode.WebviewOptions = Object.create(null); - toWebviewResource(resource: vscode.Uri): vscode.Uri { - return toWebviewResource(that._initData, this._uuid, resource); + asWebviewUri(resource: vscode.Uri): vscode.Uri { + return asWebviewUri(that._initData, this._uuid, resource); } get cspSource(): string { diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 1e229fc3dad8d..8111a7687cf94 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { validateConstraint } from 'vs/base/common/types'; -import { ICommandHandlerDescription, ICommandEvent } from 'vs/platform/commands/common/commands'; +import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -17,7 +17,6 @@ import { revive } from 'vs/base/common/marshalling'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { URI } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -36,9 +35,6 @@ export class ExtHostCommands implements ExtHostCommandsShape { readonly _serviceBrand: any; - private readonly _onDidExecuteCommand: Emitter; - readonly onDidExecuteCommand: Event; - private readonly _commands = new Map(); private readonly _proxy: MainThreadCommandsShape; private readonly _converter: CommandsConverter; @@ -50,11 +46,6 @@ export class ExtHostCommands implements ExtHostCommandsShape { @ILogService logService: ILogService ) { this._proxy = extHostRpc.getProxy(MainContext.MainThreadCommands); - this._onDidExecuteCommand = new Emitter({ - onFirstListenerDidAdd: () => this._proxy.$registerCommandListener(), - onLastListenerRemove: () => this._proxy.$unregisterCommandListener(), - }); - this.onDidExecuteCommand = Event.filter(this._onDidExecuteCommand.event, e => e.command[0] !== '_'); // filter 'private' commands this._logService = logService; this._converter = new CommandsConverter(this); this._argumentProcessors = [ @@ -119,22 +110,13 @@ export class ExtHostCommands implements ExtHostCommandsShape { }); } - $handleDidExecuteCommand(command: ICommandEvent): void { - this._onDidExecuteCommand.fire({ - command: command.commandId, - arguments: command.args.map(arg => this._argumentProcessors.reduce((r, p) => p.processArgument(r), arg)) - }); - } - executeCommand(id: string, ...args: any[]): Promise { this._logService.trace('ExtHostCommands#executeCommand', id); if (this._commands.has(id)) { // we stay inside the extension host and support // to pass any kind of parameters around - const res = this._executeContributedCommand(id, args); - this._onDidExecuteCommand.fire({ command: id, arguments: args }); - return res; + return this._executeContributedCommand(id, args); } else { // automagically convert some argument types diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index 60dae259b05c7..d654408346bac 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import * as vscode from 'vscode'; import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from './extHost.protocol'; import { DiagnosticSeverity } from './extHostTypes'; @@ -20,12 +20,12 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { private readonly _owner: string; private readonly _maxDiagnosticsPerFile: number; private readonly _onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>; - private readonly _proxy: MainThreadDiagnosticsShape; + private readonly _proxy: MainThreadDiagnosticsShape | undefined; private _isDisposed = false; private _data = new Map(); - constructor(name: string, owner: string, maxDiagnosticsPerFile: number, proxy: MainThreadDiagnosticsShape, onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>) { + constructor(name: string, owner: string, maxDiagnosticsPerFile: number, proxy: MainThreadDiagnosticsShape | undefined, onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>) { this._name = name; this._owner = owner; this._maxDiagnosticsPerFile = maxDiagnosticsPerFile; @@ -36,7 +36,9 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { dispose(): void { if (!this._isDisposed) { this._onDidChangeDiagnostics.fire(keys(this._data)); - this._proxy.$clear(this._owner); + if (this._proxy) { + this._proxy.$clear(this._owner); + } this._data = undefined!; this._isDisposed = true; } @@ -112,6 +114,9 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { this._onDidChangeDiagnostics.fire(toSync); // compute change and send to main side + if (!this._proxy) { + return; + } const entries: [URI, IMarkerData[]][] = []; for (let uri of toSync) { let marker: IMarkerData[] = []; @@ -149,7 +154,6 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { entries.push([uri, marker]); } - this._proxy.$changeMany(this._owner, entries); } @@ -157,14 +161,18 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { this._checkDisposed(); this._onDidChangeDiagnostics.fire([uri]); this._data.delete(uri.toString()); - this._proxy.$changeMany(this._owner, [[uri, undefined]]); + if (this._proxy) { + this._proxy.$changeMany(this._owner, [[uri, undefined]]); + } } clear(): void { this._checkDisposed(); this._onDidChangeDiagnostics.fire(keys(this._data)); this._data.clear(); - this._proxy.$clear(this._owner); + if (this._proxy) { + this._proxy.$clear(this._owner); + } } forEach(callback: (uri: URI, diagnostics: ReadonlyArray, collection: DiagnosticCollection) => any, thisArg?: any): void { @@ -311,4 +319,20 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { }); return res; } + + private _mirrorCollection: vscode.DiagnosticCollection | undefined; + + $acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void { + + if (!this._mirrorCollection) { + const name = '_generated_mirror'; + const collection = new DiagnosticCollection(name, name, ExtHostDiagnostics._maxDiagnosticsPerFile, undefined, this._onDidChangeDiagnostics); + this._collections.set(name, collection); + this._mirrorCollection = collection; + } + + for (const [uri, markers] of data) { + this._mirrorCollection.set(URI.revive(uri), markers.map(converter.Diagnostic.to)); + } + } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 58098d94099c7..8c903a6027c03 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import { originalFSPath } from 'vs/base/common/resources'; +import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; @@ -14,7 +14,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, IInitData, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape, IResolveAuthorityResult } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ActivatedExtension, EmptyExtension, ExtensionActivatedByAPI, ExtensionActivatedByEvent, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionContext, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; -import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; @@ -75,7 +74,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio protected readonly _instaService: IInstantiationService; protected readonly _extHostWorkspace: ExtHostWorkspace; protected readonly _extHostConfiguration: ExtHostConfiguration; - protected readonly _extHostLogService: ExtHostLogService; + protected readonly _logService: ILogService; protected readonly _mainThreadWorkspaceProxy: MainThreadWorkspaceShape; protected readonly _mainThreadTelemetryProxy: MainThreadTelemetryShape; @@ -102,7 +101,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio @IExtHostRpcService extHostContext: IExtHostRpcService, @IExtHostWorkspace extHostWorkspace: IExtHostWorkspace, @IExtHostConfiguration extHostConfiguration: IExtHostConfiguration, - @ILogService extHostLogService: ExtHostLogService, + @ILogService logService: ILogService, @IExtHostInitDataService initData: IExtHostInitDataService, @IExtensionStoragePaths storagePath: IExtensionStoragePaths ) { @@ -112,7 +111,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio this._extHostWorkspace = extHostWorkspace; this._extHostConfiguration = extHostConfiguration; - this._extHostLogService = extHostLogService; + this._logService = logService; this._disposables = new DisposableStore(); this._mainThreadWorkspaceProxy = this._extHostContext.getProxy(MainContext.MainThreadWorkspace); @@ -329,25 +328,25 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio return Promise.resolve(new EmptyExtension(ExtensionActivationTimes.NONE)); } - this._extHostLogService.info(`ExtensionService#_doActivateExtension ${extensionDescription.identifier.value} ${JSON.stringify(reason)}`); + this._logService.info(`ExtensionService#_doActivateExtension ${extensionDescription.identifier.value} ${JSON.stringify(reason)}`); const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(extensionDescription.main, activationTimesBuilder), + this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, extensionDescription.main), activationTimesBuilder), this._loadExtensionContext(extensionDescription) ]).then(values => { - return AbstractExtHostExtensionService._callActivate(this._extHostLogService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); + return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); }); } - protected abstract _loadCommonJSModule(modulePath: string, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { const globalState = new ExtensionMemento(extensionDescription.identifier.value, true, this._storage); const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); - this._extHostLogService.trace(`ExtensionService#loadExtensionContext ${extensionDescription.identifier.value}`); + this._logService.trace(`ExtensionService#loadExtensionContext ${extensionDescription.identifier.value}`); return Promise.all([ globalState.whenReady, workspaceState.whenReady, @@ -359,10 +358,10 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio workspaceState, subscriptions: [], get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, - storagePath: this._storagePath.workspaceValue(extensionDescription), - globalStoragePath: this._storagePath.globalValue(extensionDescription), + get storagePath() { return that._storagePath.workspaceValue(extensionDescription); }, + get globalStoragePath() { return that._storagePath.globalValue(extensionDescription); }, asAbsolutePath: (relativePath: string) => { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, - logPath: that._extHostLogService.getLogDirectory(extensionDescription.identifier), + get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); }, executionContext: this._initData.remote.isRemote ? ExtensionExecutionContext.Remote : ExtensionExecutionContext.Local, }); }); @@ -385,7 +384,8 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio try { activationTimesBuilder.activateCallStart(); logService.trace(`ExtensionService#_callActivateOptional ${extensionId.value}`); - const activateResult: Promise = extensionModule.activate.apply(global, [context]); + const scope = typeof global === 'object' ? global : self; // `global` is nodejs while `self` is for workers + const activateResult: Promise = extensionModule.activate.apply(scope, [context]); activationTimesBuilder.activateCallStop(); activationTimesBuilder.activateResolveStart(); @@ -478,7 +478,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio } private async _activateIfGlobPatterns(folders: ReadonlyArray, extensionId: ExtensionIdentifier, globPatterns: string[]): Promise { - this._extHostLogService.trace(`extensionHostMain#activateIfGlobPatterns: fileSearch, extension: ${extensionId.value}, entryPoint: workspaceContains`); + this._logService.trace(`extensionHostMain#activateIfGlobPatterns: fileSearch, extension: ${extensionId.value}, entryPoint: workspaceContains`); if (globPatterns.length === 0) { return Promise.resolve(undefined); @@ -525,7 +525,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio } private async _doHandleExtensionTests(): Promise { - const { extensionDevelopmentLocationURI: extensionDevelopmentLocationURI, extensionTestsLocationURI } = this._initData.environment; + const { extensionDevelopmentLocationURI, extensionTestsLocationURI } = this._initData.environment; if (!(extensionDevelopmentLocationURI && extensionTestsLocationURI && extensionTestsLocationURI.scheme === Schemas.file)) { return Promise.resolve(undefined); } @@ -536,7 +536,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio let testRunner: ITestRunner | INewTestRunner | undefined; let requireError: Error | undefined; try { - testRunner = await this._loadCommonJSModule(extensionTestsPath, new ExtensionActivationTimesBuilder(false)); + testRunner = await this._loadCommonJSModule(URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); } catch (error) { requireError = error; } @@ -605,7 +605,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio .then(() => this._handleEagerExtensions()) .then(() => this._handleExtensionTests()) .then(() => { - this._extHostLogService.info(`eager extensions activated`); + this._logService.info(`eager extensions activated`); }); } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 55c0234ee5688..6efdd4d5e8567 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -70,7 +70,8 @@ class DocumentSymbolAdapter { const element = { name: info.name || '!!MISSING: name!!', kind: typeConvert.SymbolKind.from(info.kind), - detail: undefined!, // Strict null override — avoid changing behavior + tags: info.tags && info.tags.map(typeConvert.SymbolTag.from), + detail: '', containerName: info.containerName, range: typeConvert.Range.from(info.location.range), selectionRange: typeConvert.Range.from(info.location.range), @@ -727,6 +728,7 @@ class SuggestAdapter { // a: item.label, b: typeConvert.CompletionItemKind.from(item.kind), + n: item.tags && item.tags.map(typeConvert.CompletionItemTag.from), c: item.detail, d: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), e: item.sortText, diff --git a/src/vs/workbench/api/common/extHostLogService.ts b/src/vs/workbench/api/common/extHostLogService.ts deleted file mode 100644 index d67a19801d3ae..0000000000000 --- a/src/vs/workbench/api/common/extHostLogService.ts +++ /dev/null @@ -1,34 +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 { join } from 'vs/base/common/path'; -import { ILogService, DelegatedLogService, LogLevel } from 'vs/platform/log/common/log'; -import { ExtHostLogServiceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; -import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; - -export class ExtHostLogService extends DelegatedLogService implements ILogService, ExtHostLogServiceShape { - - private _logsPath: string; - readonly logFile: URI; - - constructor( - delegate: ILogService, - logsPath: string, - ) { - super(delegate); - this._logsPath = logsPath; - this.logFile = URI.file(join(logsPath, `${ExtensionHostLogFileName}.log`)); - } - - $setLevel(level: LogLevel): void { - this.setLevel(level); - } - - getLogDirectory(extensionID: ExtensionIdentifier): string { - return join(this._logsPath, extensionID.value); - } -} diff --git a/src/vs/workbench/api/node/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts similarity index 63% rename from src/vs/workbench/api/node/extHostRequireInterceptor.ts rename to src/vs/workbench/api/common/extHostRequireInterceptor.ts index 876d6716a676a..46866f7db08d0 100644 --- a/src/vs/workbench/api/node/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -5,56 +5,64 @@ import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; -import { MainThreadKeytarShape, IEnvironment, MainThreadWindowShape, MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; +import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { endsWith } from 'vs/base/common/strings'; import { IExtensionApiFactory } from 'vs/workbench/api/common/extHost.api.impl'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { platform } from 'vs/base/common/process'; interface LoadFunction { - (request: string, parent: { filename: string; }, isMain: any): any; + (request: string): any; } interface INodeModuleFactory { readonly nodeModuleName: string | string[]; - load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any; - alternaiveModuleName?(name: string): string | undefined; + load(request: string, parent: URI, original: LoadFunction): any; + alternativeModuleName?(name: string): string | undefined; } -export class NodeModuleRequireInterceptor { - public static INSTANCE = new NodeModuleRequireInterceptor(); +export abstract class RequireInterceptor { - private readonly _factories: Map; - private readonly _alternatives: ((moduleName: string) => string | undefined)[]; + protected readonly _factories: Map; + protected readonly _alternatives: ((moduleName: string) => string | undefined)[]; - constructor() { + constructor( + private _apiFactory: IExtensionApiFactory, + private _extensionRegistry: ExtensionDescriptionRegistry, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IExtHostConfiguration private readonly _extHostConfiguration: IExtHostConfiguration, + @IExtHostExtensionService private readonly _extHostExtensionService: IExtHostExtensionService, + @IExtHostInitDataService private readonly _initData: IExtHostInitDataService + ) { this._factories = new Map(); this._alternatives = []; - this._installInterceptor(this._factories, this._alternatives); } - private _installInterceptor(factories: Map, alternatives: ((moduleName: string) => string | undefined)[]): void { - const node_module = require.__$__nodeRequire('module'); - const original = node_module._load; - node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) { - for (let alternativeModuleName of alternatives) { - let alternative = alternativeModuleName(request); - if (alternative) { - request = alternative; - break; - } - } - if (!factories.has(request)) { - return original.apply(this, arguments); - } - return factories.get(request)!.load(request, parent, isMain, original); - }; + async install(): Promise { + + this._installInterceptor(); + + const configProvider = await this._extHostConfiguration.getConfigProvider(); + const extensionPaths = await this._extHostExtensionService.getExtensionPathIndex(); + + this.register(new VSCodeNodeModuleFactory(this._apiFactory, extensionPaths, this._extensionRegistry, configProvider)); + this.register(this._instaService.createInstance(KeytarNodeModuleFactory)); + if (this._initData.remote.isRemote) { + this.register(this._instaService.createInstance(OpenNodeModuleFactory, extensionPaths)); + } } + protected abstract _installInterceptor(): void; + public register(interceptor: INodeModuleFactory): void { if (Array.isArray(interceptor.nodeModuleName)) { for (let moduleName of interceptor.nodeModuleName) { @@ -63,15 +71,17 @@ export class NodeModuleRequireInterceptor { } else { this._factories.set(interceptor.nodeModuleName, interceptor); } - if (typeof interceptor.alternaiveModuleName === 'function') { + if (typeof interceptor.alternativeModuleName === 'function') { this._alternatives.push((moduleName) => { - return interceptor.alternaiveModuleName!(moduleName); + return interceptor.alternativeModuleName!(moduleName); }); } } } -export class VSCodeNodeModuleFactory implements INodeModuleFactory { +//#region --- vscode-module + +class VSCodeNodeModuleFactory implements INodeModuleFactory { public readonly nodeModuleName = 'vscode'; private readonly _extApiImpl = new Map(); @@ -85,10 +95,10 @@ export class VSCodeNodeModuleFactory implements INodeModuleFactory { ) { } - public load(request: string, parent: { filename: string; }): any { + public load(_request: string, parent: URI): any { // get extension id from filename and api for extension - const ext = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); + const ext = this._extensionPaths.findSubstr(parent.fsPath); if (ext) { let apiImpl = this._extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier)); if (!apiImpl) { @@ -102,13 +112,18 @@ export class VSCodeNodeModuleFactory implements INodeModuleFactory { if (!this._defaultApiImpl) { let extensionPathsPretty = ''; this._extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`); - console.warn(`Could not identify extension for 'vscode' require call from ${parent.filename}. These are the extension path mappings: \n${extensionPathsPretty}`); + console.warn(`Could not identify extension for 'vscode' require call from ${parent.fsPath}. These are the extension path mappings: \n${extensionPathsPretty}`); this._defaultApiImpl = this._apiFactory(nullExtensionDescription, this._extensionRegistry, this._configProvider); } return this._defaultApiImpl; } } +//#endregion + + +//#region --- keytar-module + interface IKeytarModule { getPassword(service: string, account: string): Promise; setPassword(service: string, account: string, password: string): Promise; @@ -116,16 +131,23 @@ interface IKeytarModule { findPassword(service: string): Promise; } -export class KeytarNodeModuleFactory implements INodeModuleFactory { +class KeytarNodeModuleFactory implements INodeModuleFactory { public readonly nodeModuleName: string = 'keytar'; private alternativeNames: Set | undefined; private _impl: IKeytarModule; - constructor(mainThreadKeytar: MainThreadKeytarShape, environment: IEnvironment) { + constructor( + @IExtHostRpcService rpcService: IExtHostRpcService, + @IExtHostInitDataService initData: IExtHostInitDataService, + + ) { + const { environment } = initData; + const mainThreadKeytar = rpcService.getProxy(MainContext.MainThreadKeytar); + if (environment.appRoot) { let appRoot = environment.appRoot.fsPath; - if (process.platform === 'win32') { + if (platform === 'win32') { appRoot = appRoot.replace(/\\/g, '/'); } if (appRoot[appRoot.length - 1] === '/') { @@ -151,11 +173,11 @@ export class KeytarNodeModuleFactory implements INodeModuleFactory { }; } - public load(request: string, parent: { filename: string; }): any { + public load(_request: string, _parent: URI): any { return this._impl; } - public alternaiveModuleName(name: string): string | undefined { + public alternativeModuleName(name: string): string | undefined { const length = name.length; // We need at least something like: `?/keytar` which requires // more than 7 characters. @@ -173,6 +195,11 @@ export class KeytarNodeModuleFactory implements INodeModuleFactory { } } +//#endregion + + +//#region --- opn/open-module + interface OpenOptions { wait: boolean; app: string | string[]; @@ -186,15 +213,23 @@ interface IOpenModule { (target: string, options?: OpenOptions): Thenable; } -export class OpenNodeModuleFactory implements INodeModuleFactory { +class OpenNodeModuleFactory implements INodeModuleFactory { public readonly nodeModuleName: string[] = ['open', 'opn']; private _extensionId: string | undefined; private _original?: IOriginalOpen; private _impl: IOpenModule; + private _mainThreadTelemetry: MainThreadTelemetryShape; + + constructor( + private readonly _extensionPaths: TernarySearchTree, + @IExtHostRpcService rpcService: IExtHostRpcService, + ) { + + this._mainThreadTelemetry = rpcService.getProxy(MainContext.MainThreadTelemetry); + const mainThreadWindow = rpcService.getProxy(MainContext.MainThreadWindow); - constructor(mainThreadWindow: MainThreadWindowShape, private _mainThreadTelemerty: MainThreadTelemetryShape, private readonly _extensionPaths: TernarySearchTree) { this._impl = (target, options) => { const uri: URI = URI.parse(target); // If we have options use the original method. @@ -210,15 +245,15 @@ export class OpenNodeModuleFactory implements INodeModuleFactory { }; } - public load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any { + public load(request: string, parent: URI, original: LoadFunction): any { // get extension id from filename and api for extension - const extension = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); + const extension = this._extensionPaths.findSubstr(parent.fsPath); if (extension) { this._extensionId = extension.identifier.value; this.sendShimmingTelemetry(); } - this._original = original(request, parent, isMain); + this._original = original(request); return this._impl; } @@ -234,7 +269,7 @@ export class OpenNodeModuleFactory implements INodeModuleFactory { type ShimmingOpenClassification = { extension: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; - this._mainThreadTelemerty.$publicLog2<{ extension: string }, ShimmingOpenClassification>('shimming.open', { extension: this._extensionId }); + this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingOpenClassification>('shimming.open', { extension: this._extensionId }); } private sendNoForwardTelemetry(): void { @@ -244,6 +279,8 @@ export class OpenNodeModuleFactory implements INodeModuleFactory { type ShimmingOpenCallNoForwardClassification = { extension: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; - this._mainThreadTelemerty.$publicLog2<{ extension: string }, ShimmingOpenCallNoForwardClassification>('shimming.open.call.noForward', { extension: this._extensionId }); + this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingOpenCallNoForwardClassification>('shimming.open.call.noForward', { extension: this._extensionId }); } } + +//#endregion diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 71c8ecf346fee..23fdc7054b2c3 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -52,10 +52,21 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { private commands: ExtHostCommands, private logService: ILogService ) { + + function isTreeViewItemHandleArg(arg: any): boolean { + return arg && arg.$treeViewId && arg.$treeItemHandle; + } commands.registerArgumentProcessor({ processArgument: arg => { - if (arg && arg.$treeViewId && arg.$treeItemHandle) { + if (isTreeViewItemHandleArg(arg)) { return this.convertArgument(arg); + } else if (Array.isArray(arg) && (arg.length > 0)) { + return arg.map(item => { + if (isTreeViewItemHandleArg(item)) { + return this.convertArgument(item); + } + return item; + }); } return arg; } @@ -185,7 +196,7 @@ class ExtHostTreeView extends Disposable { constructor(private viewId: string, options: vscode.TreeViewOptions, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService, private extension: IExtensionDescription) { super(); this.dataProvider = options.treeDataProvider; - this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll }); + this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany }); if (this.dataProvider.onDidChangeTreeData) { this._register(this.dataProvider.onDidChangeTreeData(element => this._onDidChangeData.fire({ message: false, element }))); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index f7827afe487b9..6a050937ddb7d 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -28,7 +28,7 @@ import * as marked from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; -import { coalesce } from 'vs/base/common/arrays'; +import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; export interface PositionLike { @@ -113,6 +113,15 @@ export namespace DiagnosticTag { } return undefined; } + export function to(value: MarkerTag): vscode.DiagnosticTag | undefined { + switch (value) { + case MarkerTag.Unnecessary: + return types.DiagnosticTag.Unnecessary; + case MarkerTag.Deprecated: + return types.DiagnosticTag.Deprecated; + } + return undefined; + } } export namespace Diagnostic { @@ -127,6 +136,15 @@ export namespace Diagnostic { tags: Array.isArray(value.tags) ? coalesce(value.tags.map(DiagnosticTag.from)) : undefined, }; } + + export function to(value: IMarkerData): vscode.Diagnostic { + const res = new types.Diagnostic(Range.to(value), value.message, DiagnosticSeverity.to(value.severity)); + res.source = value.source; + res.code = value.code; + res.relatedInformation = value.relatedInformation && value.relatedInformation.map(DiagnosticRelatedInformation.to); + res.tags = value.tags && coalesce(value.tags.map(DiagnosticTag.to)); + return res; + } } export namespace DiagnosticRelatedInformation { @@ -538,22 +556,40 @@ export namespace SymbolKind { } } +export namespace SymbolTag { + + export function from(kind: types.SymbolTag): modes.SymbolTag { + switch (kind) { + case types.SymbolTag.Deprecated: return modes.SymbolTag.Deprecated; + } + } + + export function to(kind: modes.SymbolTag): types.SymbolTag { + switch (kind) { + case modes.SymbolTag.Deprecated: return types.SymbolTag.Deprecated; + } + } +} + export namespace WorkspaceSymbol { export function from(info: vscode.SymbolInformation): search.IWorkspaceSymbol { return { name: info.name, kind: SymbolKind.from(info.kind), + tags: info.tags && info.tags.map(SymbolTag.from), containerName: info.containerName, location: location.from(info.location) }; } export function to(info: search.IWorkspaceSymbol): types.SymbolInformation { - return new types.SymbolInformation( + const result = new types.SymbolInformation( info.name, SymbolKind.to(info.kind), info.containerName, location.to(info.location) ); + result.tags = info.tags && info.tags.map(SymbolTag.to); + return result; } } @@ -564,7 +600,8 @@ export namespace DocumentSymbol { detail: info.detail, range: Range.from(info.range), selectionRange: Range.from(info.selectionRange), - kind: SymbolKind.from(info.kind) + kind: SymbolKind.from(info.kind), + tags: info.tags ? info.tags.map(SymbolTag.from) : [] }; if (info.children) { result.children = info.children.map(from); @@ -579,6 +616,9 @@ export namespace DocumentSymbol { Range.to(info.range), Range.to(info.selectionRange), ); + if (isNonEmptyArray(info.tags)) { + result.tags = info.tags.map(SymbolTag.to); + } if (info.children) { result.children = info.children.map(to) as any; } @@ -663,6 +703,21 @@ export namespace CompletionContext { } } +export namespace CompletionItemTag { + + export function from(kind: types.CompletionItemTag): modes.CompletionItemTag { + switch (kind) { + case types.CompletionItemTag.Deprecated: return modes.CompletionItemTag.Deprecated; + } + } + + export function to(kind: modes.CompletionItemTag): types.CompletionItemTag { + switch (kind) { + case modes.CompletionItemTag.Deprecated: return types.CompletionItemTag.Deprecated; + } + } +} + export namespace CompletionItemKind { export function from(kind: types.CompletionItemKind | undefined): modes.CompletionItemKind { @@ -734,6 +789,7 @@ export namespace CompletionItem { const result = new types.CompletionItem(suggestion.label); result.insertText = suggestion.insertText; result.kind = CompletionItemKind.to(suggestion.kind); + result.tags = suggestion.tags && suggestion.tags.map(CompletionItemTag.to); result.detail = suggestion.detail; result.documentation = htmlContent.isMarkdownString(suggestion.documentation) ? MarkdownString.to(suggestion.documentation) : suggestion.documentation; result.sortText = suggestion.sortText; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9b0cd9c9cc88d..4c92eddcaf79b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -979,6 +979,10 @@ export enum SymbolKind { TypeParameter = 25 } +export enum SymbolTag { + Deprecated = 1, +} + @es5ClassCompat export class SymbolInformation { @@ -991,6 +995,7 @@ export class SymbolInformation { name: string; location!: Location; kind: SymbolKind; + tags?: SymbolTag[]; containerName: string | undefined; constructor(name: string, kind: SymbolKind, containerName: string | undefined, location: Location); @@ -1041,6 +1046,7 @@ export class DocumentSymbol { name: string; detail: string; kind: SymbolKind; + tags?: SymbolTag[]; range: Range; selectionRange: Range; children: DocumentSymbol[]; @@ -1075,6 +1081,8 @@ export class CodeAction { kind?: CodeActionKind; + isPreferred?: boolean; + constructor(title: string, kind?: CodeActionKind) { this.title = title; this.kind = kind; @@ -1306,11 +1314,16 @@ export enum CompletionItemKind { TypeParameter = 24 } +export enum CompletionItemTag { + Deprecated = 1, +} + @es5ClassCompat export class CompletionItem implements vscode.CompletionItem { label: string; kind?: CompletionItemKind; + tags?: CompletionItemTag[]; detail?: string; documentation?: string | MarkdownString; sortText?: string; @@ -2164,6 +2177,24 @@ export class FunctionBreakpoint extends Breakpoint { } } +@es5ClassCompat +export class DataBreakpoint extends Breakpoint { + readonly label: string; + readonly dataId: string; + readonly canPersist: boolean; + + constructor(label: string, dataId: string, canPersist: boolean, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + super(enabled, condition, hitCondition, logMessage); + if (!dataId) { + throw illegalArgument('dataId'); + } + this.label = label; + this.dataId = dataId; + this.canPersist = canPersist; + } +} + + @es5ClassCompat export class DebugAdapterExecutable implements vscode.DebugAdapterExecutable { readonly command: string; diff --git a/src/vs/workbench/api/common/extHostUrls.ts b/src/vs/workbench/api/common/extHostUrls.ts index 778d957cbc775..6b6f8081ae58f 100644 --- a/src/vs/workbench/api/common/extHostUrls.ts +++ b/src/vs/workbench/api/common/extHostUrls.ts @@ -55,4 +55,8 @@ export class ExtHostUrls implements ExtHostUrlsShape { return Promise.resolve(undefined); } + + async createAppUri(extensionId: ExtensionIdentifier, options?: vscode.AppUriOptions): Promise { + return URI.revive(await this._proxy.$createAppUri(extensionId, options)); + } } diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index aaa6a49734cea..168e49abca734 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -5,15 +5,15 @@ import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import * as vscode from 'vscode'; -import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState } from './extHost.protocol'; +import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; import { Disposable } from './extHostTypes'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import * as modes from 'vs/editor/common/modes'; -import { WebviewInitData, toWebviewResource } from 'vs/workbench/api/common/shared/webview'; -import { generateUuid } from 'vs/base/common/uuid'; type IconPath = URI | { light: URI, dark: URI }; @@ -35,8 +35,8 @@ export class ExtHostWebview implements vscode.Webview { this._onMessageEmitter.dispose(); } - public toWebviewResource(resource: vscode.Uri): vscode.Uri { - return toWebviewResource(this._initData, this._handle, resource); + public asWebviewUri(resource: vscode.Uri): vscode.Uri { + return asWebviewUri(this._initData, this._handle, resource); } public get cspSource(): string { @@ -89,11 +89,12 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel { private readonly _options: vscode.WebviewPanelOptions; private readonly _webview: ExtHostWebview; - private _isDisposed: boolean = false; private _viewColumn: vscode.ViewColumn | undefined; private _visible: boolean = true; private _active: boolean = true; + _isDisposed: boolean = false; + readonly _onDisposeEmitter = new Emitter(); public readonly onDidDispose: Event = this._onDisposeEmitter.event; @@ -297,21 +298,38 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } } - public $onDidChangeWebviewPanelViewState( - handle: WebviewPanelHandle, - newState: WebviewPanelViewState - ): void { - const panel = this.getWebviewPanel(handle); - if (!panel) { - return; - } + public $onDidChangeWebviewPanelViewStates(newStates: WebviewPanelViewStateData): void { + const handles = Object.keys(newStates); + // Notify webviews of state changes in the following order: + // - Non-visible + // - Visible + // - Active + handles.sort((a, b) => { + const stateA = newStates[a]; + const stateB = newStates[b]; + if (stateA.active) { + return 1; + } + if (stateB.active) { + return -1; + } + return (+stateA.visible) - (+stateB.visible); + }); - const viewColumn = typeConverters.ViewColumn.to(newState.position); - if (panel.active !== newState.active || panel.visible !== newState.visible || panel.viewColumn !== viewColumn) { - panel._setActive(newState.active); - panel._setVisible(newState.visible); - panel._setViewColumn(viewColumn); - panel._onDidChangeViewStateEmitter.fire({ webviewPanel: panel }); + for (const handle of handles) { + const panel = this.getWebviewPanel(handle); + if (!panel || panel._isDisposed) { + continue; + } + + const newState = newStates[handle]; + const viewColumn = typeConverters.ViewColumn.to(newState.position); + if (panel.active !== newState.active || panel.visible !== newState.visible || panel.viewColumn !== viewColumn) { + panel._setActive(newState.active); + panel._setVisible(newState.visible); + panel._setViewColumn(viewColumn); + panel._onDidChangeViewStateEmitter.fire({ webviewPanel: panel }); + } } } diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 65395d5ed4cd7..cb6cddcb57d8f 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -12,7 +12,7 @@ import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } fr import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { MenuId, MenuRegistry, ILocalizedString, IMenuItem } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; namespace schema { @@ -329,7 +329,7 @@ namespace schema { }; } -let _commandRegistrations: IDisposable[] = []; +const _commandRegistrations = new DisposableStore(); export const commandsExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'commands', @@ -338,7 +338,7 @@ export const commandsExtensionPoint = ExtensionsRegistry.registerExtensionPoint< commandsExtensionPoint.setHandler(extensions => { - function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser, disposables: IDisposable[]) { + function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser) { if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) { return; @@ -368,20 +368,20 @@ commandsExtensionPoint.setHandler(extensions => { precondition: ContextKeyExpr.deserialize(enablement), iconLocation: absoluteIcon }); - disposables.push(registration); + _commandRegistrations.add(registration); } // remove all previous command registrations - _commandRegistrations = dispose(_commandRegistrations); + _commandRegistrations.clear(); - for (let extension of extensions) { + for (const extension of extensions) { const { value } = extension; - if (Array.isArray(value)) { - for (let command of value) { - handleCommand(command, extension, _commandRegistrations); + if (Array.isArray(value)) { + for (const command of value) { + handleCommand(command, extension); } } else { - handleCommand(value, extension, _commandRegistrations); + handleCommand(value, extension); } } }); diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index 3969d74dbe1bd..9740cd21a6fd1 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -11,7 +11,7 @@ export interface WebviewInitData { readonly webviewCspSource: string; } -export function toWebviewResource( +export function asWebviewUri( initData: WebviewInitData, uuid: string, resource: vscode.Uri diff --git a/src/vs/workbench/api/node/extHost.services.ts b/src/vs/workbench/api/node/extHost.services.ts index 153413e086e8c..a227d8a67b3f7 100644 --- a/src/vs/workbench/api/node/extHost.services.ts +++ b/src/vs/workbench/api/node/extHost.services.ts @@ -24,8 +24,11 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; // register singleton services +registerSingleton(ILogService, ExtHostLogService); registerSingleton(IExtHostOutputService, ExtHostOutputService2); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostDecorations, ExtHostDecorations); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 87adb02d7483c..c82f788a37098 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -14,7 +14,7 @@ import { IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; import * as vscode from 'vscode'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint } from 'vs/workbench/api/common/extHostTypes'; import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; @@ -248,7 +248,8 @@ export class ExtHostDebugService implements IExtHostDebugService, ExtHostDebugSe // unregister with VS Code const ids = breakpoints.filter(bp => bp instanceof SourceBreakpoint).map(bp => bp.id); const fids = breakpoints.filter(bp => bp instanceof FunctionBreakpoint).map(bp => bp.id); - return this._debugServiceProxy.$unregisterBreakpoints(ids, fids); + const dids = breakpoints.filter(bp => bp instanceof DataBreakpoint).map(bp => bp.id); + return this._debugServiceProxy.$unregisterBreakpoints(ids, fids, dids); } public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, parentSession?: vscode.DebugSession): Promise { @@ -554,6 +555,8 @@ export class ExtHostDebugService implements IExtHostDebugService, ExtHostDebugSe let bp: vscode.Breakpoint; if (bpd.type === 'function') { bp = new FunctionBreakpoint(bpd.functionName, bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage); + } else if (bpd.type === 'data') { + bp = new DataBreakpoint(bpd.label, bpd.dataId, bpd.canPersist, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage); } else { const uri = URI.revive(bpd.uri); bp = new SourceBreakpoint(new Location(uri, new Position(bpd.line, bpd.character)), bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage); diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index efbf84cf6413e..8e96e6738ec20 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -4,13 +4,41 @@ *--------------------------------------------------------------------------------------------*/ import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl'; -import { NodeModuleRequireInterceptor, VSCodeNodeModuleFactory, KeytarNodeModuleFactory, OpenNodeModuleFactory } from 'vs/workbench/api/node/extHostRequireInterceptor'; +import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator'; import { connectProxyResolver } from 'vs/workbench/services/extensions/node/proxyResolver'; import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadService'; import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; + +class NodeModuleRequireInterceptor extends RequireInterceptor { + + protected _installInterceptor(): void { + const that = this; + const node_module = require.__$__nodeRequire('module'); + const original = node_module._load; + node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) { + for (let alternativeModuleName of that._alternatives) { + let alternative = alternativeModuleName(request); + if (alternative) { + request = alternative; + break; + } + } + if (!that._factories.has(request)) { + return original.apply(this, arguments); + } + return that._factories.get(request)!.load( + request, + URI.file(parent.filename), + request => original.apply(this, [request, parent, isMain]) + ); + }; + } +} export class ExtHostExtensionService extends AbstractExtHostExtensionService { @@ -28,29 +56,34 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } // Module loading tricks - const configProvider = await this._extHostConfiguration.getConfigProvider(); - const extensionPaths = await this.getExtensionPathIndex(); - NodeModuleRequireInterceptor.INSTANCE.register(new VSCodeNodeModuleFactory(extensionApiFactory, extensionPaths, this._registry, configProvider)); - NodeModuleRequireInterceptor.INSTANCE.register(new KeytarNodeModuleFactory(this._extHostContext.getProxy(MainContext.MainThreadKeytar), this._initData.environment)); - if (this._initData.remote.isRemote) { - NodeModuleRequireInterceptor.INSTANCE.register(new OpenNodeModuleFactory( - this._extHostContext.getProxy(MainContext.MainThreadWindow), - this._extHostContext.getProxy(MainContext.MainThreadTelemetry), - extensionPaths - )); - } + const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); + await interceptor.install(); // Do this when extension service exists, but extensions are not being activated yet. - await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._extHostLogService, this._mainThreadTelemetryProxy); + const configProvider = await this._extHostConfiguration.getConfigProvider(); + await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy); + // Use IPC messages to forward console-calls, note that the console is + // already patched to use`process.send()` + const nativeProcessSend = process.send!; + const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole); + process.send = (...args: any[]) => { + if (args.length === 0 || !args[0] || args[0].type !== '__$console') { + return nativeProcessSend.apply(process, args); + } + mainThreadConsole.$logExtensionHostMessage(args[0]); + }; } - protected _loadCommonJSModule(modulePath: string, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + if (module.scheme !== Schemas.file) { + throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); + } let r: T | null = null; activationTimesBuilder.codeLoadingStart(); - this._extHostLogService.info(`ExtensionService#loadCommonJSModule ${modulePath}`); + this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`); try { - r = require.__$__nodeRequire(modulePath); + r = require.__$__nodeRequire(module.fsPath); } catch (e) { return Promise.reject(e); } finally { diff --git a/src/vs/workbench/api/node/extHostLogService.ts b/src/vs/workbench/api/node/extHostLogService.ts new file mode 100644 index 0000000000000..f29d417620ded --- /dev/null +++ b/src/vs/workbench/api/node/extHostLogService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { join } from 'vs/base/common/path'; +import { ILogService, DelegatedLogService, LogLevel } from 'vs/platform/log/common/log'; +import { ExtHostLogServiceShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { Schemas } from 'vs/base/common/network'; +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; + +export class ExtHostLogService extends DelegatedLogService implements ILogService, ExtHostLogServiceShape { + + constructor( + @IExtHostInitDataService initData: IExtHostInitDataService, + @IExtHostOutputService extHostOutputService: IExtHostOutputService + ) { + if (initData.logsLocation.scheme !== Schemas.file) { throw new Error('Only file-logging supported'); } + super(new SpdLogService(ExtensionHostLogFileName, initData.logsLocation.fsPath, initData.logLevel)); + + // Register an output channel for exthost log + extHostOutputService.createOutputChannelFromLogFile( + initData.remote.isRemote ? localize('remote extension host Log', "Remote Extension Host") : localize('extension host Log', "Extension Host"), + URI.file(join(initData.logsLocation.fsPath, `${ExtensionHostLogFileName}.log`)) + ); + } + + $setLevel(level: LogLevel): void { + this.setLevel(level); + } +} diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 647a2be3f08db..cf9b522ef1113 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -444,6 +444,14 @@ export class ExtHostTask implements ExtHostTaskShape { if (dto === undefined) { return Promise.reject(new Error('Task is not valid')); } + + // If this task is a custom execution, then we need to save it away + // in the provided custom execution map that is cleaned up after the + // task is executed. + if (CustomExecution2DTO.is(dto.execution)) { + await this.addCustomExecution2(dto, task); + } + return this._proxy.$executeTask(dto).then(value => this.getTaskExecution(value, task)); } } @@ -529,11 +537,6 @@ export class ExtHostTask implements ExtHostTaskShape { return Promise.reject(new Error('no handler found')); } - // For custom execution tasks, we need to store the execution objects locally - // since we obviously cannot send callback functions through the proxy. - // So, clear out any existing ones. - this._providedCustomExecutions2.clear(); - // Set up a list of task ID promises that we can wait on // before returning the provided tasks. The ensures that // our task IDs are calculated for any custom execution tasks. @@ -692,5 +695,17 @@ export class ExtHostTask implements ExtHostTaskShape { if (extensionCallback2) { this._activeCustomExecutions2.delete(execution.id); } + + const lastCustomExecution = this._providedCustomExecutions2.get(execution.id); + // Technically we don't really need to do this, however, if an extension + // is executing a task through "executeTask" over and over again + // with different properties in the task definition, then this list + // could grow indefinitely, something we don't want. + this._providedCustomExecutions2.clear(); + // We do still need to hang on to the last custom execution so that the + // Rerun Task command doesn't choke when it tries to rerun a custom execution + if (lastCustomExecution) { + this._providedCustomExecutions2.set(execution.id, lastCustomExecution); + } } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 99a7cc8309ca2..ecce50a23f39d 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -497,6 +497,7 @@ export class ExtHostTerminalService implements IExtHostTerminalService, ExtHostT const terminalConfig = configProvider.getConfiguration('terminal.integrated'); const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), lastActiveWorkspace ? lastActiveWorkspace : undefined, this._variableResolver, activeWorkspaceRootUri, terminalConfig.cwd, this._logService); + shellLaunchConfig.cwd = initialCwd; const envFromConfig = this._apiInspectConfigToPlain(configProvider.getConfiguration('terminal.integrated').inspect(`env.${platformKey}`)); const baseEnv = terminalConfig.get('inheritEnv', true) ? process.env as platform.IProcessEnvironment : await this._getNonInheritedEnv(); @@ -737,7 +738,7 @@ class ExtHostPseudoterminal implements ITerminalChildProcess { // Attach the listeners this._pty.onDidWrite(e => this._onProcessData.fire(e)); if (this._pty.onDidClose) { - this._pty.onDidClose(e => this._onProcessExit.fire(0)); + this._pty.onDidClose(e => this._onProcessExit.fire(e || 0)); } if (this._pty.onDidOverrideDimensions) { this._pty.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : e)); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts new file mode 100644 index 0000000000000..4fcb6db76f824 --- /dev/null +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl'; +import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { endsWith, startsWith } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; + +class ExportsTrap { + + static readonly Instance = new ExportsTrap(); + + private readonly _names: string[] = []; + private readonly _exports = new Map(); + + private constructor() { + + const exportsProxy = new Proxy({}, { + set: (target: any, p: PropertyKey, value: any) => { + // store in target + target[p] = value; + // store in named-bucket + const name = this._names[this._names.length - 1]; + this._exports.get(name)![p] = value; + return true; + } + }); + + + const moduleProxy = new Proxy({}, { + + get: (target: any, p: PropertyKey) => { + if (p === 'exports') { + return exportsProxy; + } + + return target[p]; + }, + + set: (target: any, p: PropertyKey, value: any) => { + // store in target + target[p] = value; + + // override bucket + if (p === 'exports') { + const name = this._names[this._names.length - 1]; + this._exports.set(name, value); + } + return true; + } + }); + + (self).exports = exportsProxy; + (self).module = moduleProxy; + } + + add(name: string) { + this._exports.set(name, Object.create(null)); + this._names.push(name); + + return { + claim: () => { + const result = this._exports.get(name); + this._exports.delete(name); + this._names.pop(); + return result; + } + }; + } +} + +class WorkerRequireInterceptor extends RequireInterceptor { + + _installInterceptor() { } + + getModule(request: string, parent: URI): undefined | any { + for (let alternativeModuleName of this._alternatives) { + let alternative = alternativeModuleName(request); + if (alternative) { + request = alternative; + break; + } + } + + if (this._factories.has(request)) { + return this._factories.get(request)!.load(request, parent, () => { throw new Error('CANNOT LOAD MODULE from here.'); }); + } + return undefined; + } +} + +export class ExtHostExtensionService extends AbstractExtHostExtensionService { + + private _fakeModules: WorkerRequireInterceptor; + + protected async _beforeAlmostReadyToRunExtensions(): Promise { + // initialize API and register actors + const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); + this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); + await this._fakeModules.install(); + } + + protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + + (self).window = self; // <- that's improper but might help extensions that aren't authored correctly + + // FAKE require function that only works for the vscode-module + const moduleStack: URI[] = []; + (self).require = (mod: string) => { + + const parent = moduleStack[moduleStack.length - 1]; + const result = this._fakeModules.getModule(mod, parent); + + if (result !== undefined) { + return result; + } + + if (!startsWith(mod, '.')) { + throw new Error(`Cannot load module '${mod}'`); + } + + const next = joinPath(parent, '..', ensureSuffix(mod, '.js')); + moduleStack.push(next); + const trap = ExportsTrap.Instance.add(next.toString()); + importScripts(next.toString(true)); + moduleStack.pop(); + + return trap.claim(); + }; + + try { + activationTimesBuilder.codeLoadingStart(); + module = module.with({ path: ensureSuffix(module.path, '.js') }); + moduleStack.push(module); + const trap = ExportsTrap.Instance.add(module.toString()); + importScripts(module.toString(true)); + moduleStack.pop(); + return Promise.resolve(trap.claim()); + + } finally { + activationTimesBuilder.codeLoadingStop(); + } + } + + async $setRemoteEnvironment(_env: { [key: string]: string | null }): Promise { + throw new Error('Not supported'); + } +} + +function ensureSuffix(path: string, suffix: string): string { + return endsWith(path, suffix) ? path : path + suffix; +} diff --git a/src/vs/workbench/api/worker/extHostLogService.ts b/src/vs/workbench/api/worker/extHostLogService.ts new file mode 100644 index 0000000000000..d29f41ec939e2 --- /dev/null +++ b/src/vs/workbench/api/worker/extHostLogService.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService, LogLevel, AbstractLogService } from 'vs/platform/log/common/log'; +import { ExtHostLogServiceShape, MainThreadLogShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { joinPath } from 'vs/base/common/resources'; +import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; +import { UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; + +export class ExtHostLogService extends AbstractLogService implements ILogService, ExtHostLogServiceShape { + + _serviceBrand: any; + + private readonly _proxy: MainThreadLogShape; + private readonly _logFile: UriComponents; + + constructor( + @IExtHostRpcService rpc: IExtHostRpcService, + @IExtHostInitDataService initData: IExtHostInitDataService, + @IExtHostOutputService extHostOutputService: IExtHostOutputService + ) { + super(); + const logFile = joinPath(initData.logsLocation, `${ExtensionHostLogFileName}.log`); + this._proxy = rpc.getProxy(MainContext.MainThreadLog); + this._logFile = logFile.toJSON(); + this.setLevel(initData.logLevel); + extHostOutputService.createOutputChannelFromLogFile(localize('name', "Worker Extension Host"), logFile); + } + + $setLevel(level: LogLevel): void { + this.setLevel(level); + } + + trace(_message: string, ..._args: any[]): void { + if (this.getLevel() <= LogLevel.Trace) { + this._proxy.$log(this._logFile, LogLevel.Trace, Array.from(arguments)); + } + } + + debug(_message: string, ..._args: any[]): void { + if (this.getLevel() <= LogLevel.Debug) { + this._proxy.$log(this._logFile, LogLevel.Debug, Array.from(arguments)); + } + } + + info(_message: string, ..._args: any[]): void { + if (this.getLevel() <= LogLevel.Info) { + this._proxy.$log(this._logFile, LogLevel.Info, Array.from(arguments)); + } + } + + warn(_message: string, ..._args: any[]): void { + if (this.getLevel() <= LogLevel.Warning) { + this._proxy.$log(this._logFile, LogLevel.Warning, Array.from(arguments)); + } + } + + error(_message: string | Error, ..._args: any[]): void { + if (this.getLevel() <= LogLevel.Error) { + this._proxy.$log(this._logFile, LogLevel.Error, Array.from(arguments)); + } + } + + critical(_message: string | Error, ..._args: any[]): void { + if (this.getLevel() <= LogLevel.Critical) { + this._proxy.$log(this._logFile, LogLevel.Critical, Array.from(arguments)); + } + } +} diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index cf1d6b5b60f46..b99f800164d1f 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -113,7 +113,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array r.resource.fsPath === file.path) /* prevent duplicates */) { + if (file && file.path /* Electron only */ && !resources.some(r => r.resource.fsPath === file.path) /* prevent duplicates */) { try { resources.push({ resource: URI.file(file.path), isExternal: true }); } catch (error) { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 9db4979a3aaa8..35a19a119b411 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -317,40 +317,34 @@ class ResourceLabelWidget extends IconLabel { } notifyFormattersChange(): void { - if (this.label && this.label.resource) { - this.setFile(this.label.resource, this.options); - } this.render(false); } setResource(label: IResourceLabelProps, options?: IResourceLabelOptions): void { - const hasResourceChanged = this.hasResourceChanged(label, options); + const hasPathLabelChanged = this.hasPathLabelChanged(label, options); + const clearIconCache = this.clearIconCache(label, options); this.label = label; this.options = options; - if (hasResourceChanged) { + if (hasPathLabelChanged) { this.computedPathLabel = undefined; // reset path label due to resource change } - this.render(hasResourceChanged); + this.render(clearIconCache); } - private hasResourceChanged(label: IResourceLabelProps, options?: IResourceLabelOptions): boolean { - const newResource = label ? label.resource : undefined; + private clearIconCache(newLabel: IResourceLabelProps, newOptions?: IResourceLabelOptions): boolean { + const newResource = newLabel ? newLabel.resource : undefined; const oldResource = this.label ? this.label.resource : undefined; - const newFileKind = options ? options.fileKind : undefined; + const newFileKind = newOptions ? newOptions.fileKind : undefined; const oldFileKind = this.options ? this.options.fileKind : undefined; if (newFileKind !== oldFileKind) { return true; // same resource but different kind (file, folder) } - if (newResource && this.computedPathLabel !== this.labelService.getUriLabel(newResource)) { - return true; - } - if (newResource && oldResource) { return newResource.toString() !== oldResource.toString(); } @@ -362,6 +356,12 @@ class ResourceLabelWidget extends IconLabel { return true; } + private hasPathLabelChanged(newLabel: IResourceLabelProps, newOptions?: IResourceLabelOptions): boolean { + const newResource = newLabel ? newLabel.resource : undefined; + + return !!newResource && this.computedPathLabel !== this.labelService.getUriLabel(newResource); + } + setEditor(editor: IEditorInput, options?: IResourceLabelOptions): void { this.setResource({ resource: toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }), @@ -460,6 +460,7 @@ class ResourceLabelWidget extends IconLabel { } iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); } + if (this.options && this.options.extraClasses) { iconLabelOptions.extraClasses!.push(...this.options.extraClasses); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 7f82e7e7562a3..92ef4ff66f8c3 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -46,6 +46,9 @@ enum Settings { PANEL_POSITION = 'workbench.panel.defaultLocation', ZEN_MODE_RESTORE = 'zenMode.restore', + + // TODO @misolori update this when finished + OCTICONS_UPDATE_ENABLED = 'workbench.octiconsUpdate.enabled', } enum Storage { @@ -173,6 +176,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi wasSideBarVisible: false, wasPanelVisible: false, transitionDisposables: new DisposableStore() + }, + + // TODO @misolori update this when finished + octiconsUpdate: { + enabled: false } }; @@ -314,6 +322,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const newMenubarVisibility = this.configurationService.getValue(Settings.MENUBAR_VISIBLE); this.setMenubarVisibility(newMenubarVisibility, !!skipLayout); + // TODO @misolori update this when finished + const newOcticonsUpdate = this.configurationService.getValue(Settings.OCTICONS_UPDATE_ENABLED); + this.setOcticonsUpdate(newOcticonsUpdate); + } private setSideBarPosition(position: Position): void { @@ -426,6 +438,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Zen mode enablement this.state.zenMode.restore = this.storageService.getBoolean(Storage.ZEN_MODE_ENABLED, StorageScope.WORKSPACE, false) && this.configurationService.getValue(Settings.ZEN_MODE_RESTORE); + // TODO @misolori update this when finished + this.state.octiconsUpdate.enabled = this.configurationService.getValue(Settings.OCTICONS_UPDATE_ENABLED); + this.setOcticonsUpdate(this.state.octiconsUpdate.enabled); + } private resolveEditorsToOpen(fileService: IFileService): Promise | IResourceEditor[] { @@ -601,10 +617,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // To properly reset line numbers we need to read the configuration for each editor respecting it's uri. if (!lineNumbers && isCodeEditor(editor) && editor.hasModel()) { const model = editor.getModel(); - this.configurationService.getValue('editor.lineNumbers', { resource: model.uri }); - } else { - editor.updateOptions({ lineNumbers }); + lineNumbers = this.configurationService.getValue('editor.lineNumbers', { resource: model.uri }); } + if (!lineNumbers) { + lineNumbers = this.configurationService.getValue('editor.lineNumbers'); + } + + editor.updateOptions({ lineNumbers }); }); // Check if zen mode transitioned to full screen and if now we are out of zen mode @@ -729,6 +748,19 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } + // TODO @misolori update this when finished + private setOcticonsUpdate(enabled: boolean): void { + this.state.octiconsUpdate.enabled = enabled; + + // Update DOM + if (enabled) { + document.body.dataset.octiconsUpdate = 'enabled'; + } else { + document.body.dataset.octiconsUpdate = ''; + } + + } + protected createWorkbenchLayout(instantiationService: IInstantiationService): void { const titleBar = this.getPart(Parts.TITLEBAR_PART); const editorPart = this.getPart(Parts.EDITOR_PART); @@ -1177,10 +1209,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private createGridDescriptor(): ISerializedGrid { - const width = this.storageService.getNumber(Storage.GRID_WIDTH, StorageScope.GLOBAL, 600); - const height = this.storageService.getNumber(Storage.GRID_HEIGHT, StorageScope.GLOBAL, 400); - const sideBarSize = this.storageService.getNumber(Storage.SIDEBAR_SIZE, StorageScope.GLOBAL, 300); - const panelSize = this.storageService.getNumber(Storage.PANEL_SIZE, StorageScope.GLOBAL, 300); + const workbenchDimensions = getClientArea(this.parent); + const width = this.storageService.getNumber(Storage.GRID_WIDTH, StorageScope.GLOBAL, workbenchDimensions.width); + const height = this.storageService.getNumber(Storage.GRID_HEIGHT, StorageScope.GLOBAL, workbenchDimensions.height); + // At some point, we will not fall back to old keys from legacy layout, but for now, let's migrate the keys + const sideBarSize = this.storageService.getNumber(Storage.SIDEBAR_SIZE, StorageScope.GLOBAL, this.storageService.getNumber('workbench.sidebar.width', StorageScope.GLOBAL, Math.min(workbenchDimensions.width / 4, 300))!); + const panelSize = this.storageService.getNumber(Storage.PANEL_SIZE, StorageScope.GLOBAL, this.storageService.getNumber(this.state.panel.position === Position.BOTTOM ? 'workbench.panel.height' : 'workbench.panel.width', StorageScope.GLOBAL, workbenchDimensions.height / 3)); const titleBarHeight = this.titleBarPartView.minimumHeight; const statusBarHeight = this.statusBarPartView.minimumHeight; @@ -1261,4 +1295,3 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.disposed = true; } } - diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index d73545fb3ef34..5c4c70a2e75ca 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -171,7 +171,7 @@ export class PlaceHolderViewletActivityAction extends ViewletActivityAction { super({ id, name: id, cssClass: `extensionViewlet-placeholder-${id.replace(/\./g, '-')}` }, viewletService, layoutService, telemetryService); const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${this.class}`; // Generate Placeholder CSS to show the icon in the activity bar - DOM.createCSSRule(iconClass, `-webkit-mask: url('${DOM.asDomUri(iconUrl) || ''}') no-repeat 50% 50%; -webkit-mask-size: 24px;`); + DOM.createCSSRule(iconClass, `-webkit-mask: ${DOM.asCSSUrl(iconUrl)} no-repeat 50% 50%; -webkit-mask-size: 24px;`); } setActivity(activity: IActivity): void { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts index 85aed7a9bf40e..0bd6c69fbdc3f 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -71,6 +71,7 @@ export abstract class BreadcrumbsConfig { static FilePath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.filePath'); static SymbolPath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.symbolPath'); static SymbolSortOrder = BreadcrumbsConfig._stub<'position' | 'name' | 'type'>('breadcrumbs.symbolSortOrder'); + static Icons = BreadcrumbsConfig._stub('breadcrumbs.icons'); static FileExcludes = BreadcrumbsConfig._stub('files.exclude'); @@ -160,6 +161,11 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('symbolSortOrder.name', "Show symbol outline in alphabetical order."), localize('symbolSortOrder.type', "Show symbol outline in symbol type order."), ] + }, + 'breadcrumbs.icons': { + description: localize('icons', "Render breadcrumb items with icons."), + type: 'boolean', + default: true } } }); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 0614722d53682..39927e683a4d7 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -69,7 +69,9 @@ class Item extends BreadcrumbsItem { return false; } if (this.element instanceof FileElement && other.element instanceof FileElement) { - return isEqual(this.element.uri, other.element.uri, false); + return (isEqual(this.element.uri, other.element.uri, false) && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons); } if (this.element instanceof TreeElement && other.element instanceof TreeElement) { return this.element.id === other.element.id; @@ -143,6 +145,7 @@ export class BreadcrumbsControl { private readonly _ckBreadcrumbsActive: IContextKey; private readonly _cfUseQuickPick: BreadcrumbsConfig; + private readonly _cfShowIcons: BreadcrumbsConfig; readonly domNode: HTMLDivElement; private readonly _widget: BreadcrumbsWidget; @@ -185,6 +188,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService); + this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(_configurationService); this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget)); } @@ -196,6 +200,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible.reset(); this._ckBreadcrumbsActive.reset(); this._cfUseQuickPick.dispose(); + this._cfShowIcons.dispose(); this._widget.dispose(); this.domNode.remove(); } @@ -246,15 +251,23 @@ export class BreadcrumbsControl { dom.toggleClass(this.domNode, 'backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\'); const updateBreadcrumbs = () => { - const items = model.getElements().map(element => new Item(element, this._options, this._instantiationService)); + const showIcons = this._cfShowIcons.getValue(); + const options: IBreadcrumbsControlOptions = { + ...this._options, + showFileIcons: this._options.showFileIcons && showIcons, + showSymbolIcons: this._options.showSymbolIcons && showIcons + }; + const items = model.getElements().map(element => new Item(element, options, this._instantiationService)); this._widget.setItems(items); this._widget.reveal(items[items.length - 1]); }; const listener = model.onDidUpdate(updateBreadcrumbs); + const configListener = this._cfShowIcons.onDidChange(updateBreadcrumbs); updateBreadcrumbs(); this._breadcrumbsDisposables.clear(); this._breadcrumbsDisposables.add(model); this._breadcrumbsDisposables.add(listener); + this._breadcrumbsDisposables.add(configListener); // close picker on hide/update this._breadcrumbsDisposables.add({ diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 1c4f92cd45982..73b3e5f76646f 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -9,7 +9,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { size } from 'vs/base/common/collections'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual, dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -36,14 +36,14 @@ type FileInfo = { path: FileElement[], folder?: IWorkspaceFolder }; export class EditorBreadcrumbsModel { - private readonly _disposables: IDisposable[] = []; + private readonly _disposables = new DisposableStore(); private readonly _fileInfo: FileInfo; private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>; private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>; private _outlineElements: Array = []; - private _outlineDisposables: IDisposable[] = []; + private _outlineDisposables = new DisposableStore(); private _onDidUpdate = new Emitter(); readonly onDidUpdate: Event = this._onDidUpdate.event; @@ -58,8 +58,8 @@ export class EditorBreadcrumbsModel { 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._disposables.add(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); + this._disposables.add(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(this._uri, workspaceService); this._bindToEditor(); @@ -69,7 +69,7 @@ export class EditorBreadcrumbsModel { dispose(): void { this._cfgFilePath.dispose(); this._cfgSymbolPath.dispose(); - dispose(this._disposables); + this._disposables.dispose(); } isRelative(): boolean { @@ -133,20 +133,27 @@ export class EditorBreadcrumbsModel { 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(Event.debounce(this._editor.onDidChangeModelContent, _ => _, 350)(_ => this._updateOutline(true))); + // update as language, model, providers changes + this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); + this._disposables.add(this._editor.onDidChangeModel(_ => this._updateOutline())); + this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); + + // update soon'ish as model content change + const updateSoon = new TimeoutTimer(); + this._disposables.add(updateSoon); + this._disposables.add(this._editor.onDidChangeModelContent(_ => { + const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); + updateSoon.cancelAndSet(() => this._updateOutline(true), timeout); + })); this._updateOutline(); // stop when editor dies - this._disposables.push(this._editor.onDidDispose(() => this._outlineDisposables = dispose(this._outlineDisposables))); + this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); } private _updateOutline(didChangeContent?: boolean): void { - this._outlineDisposables = dispose(this._outlineDisposables); + this._outlineDisposables.clear(); if (!didChangeContent) { this._updateOutlineElements([]); } @@ -162,7 +169,7 @@ export class EditorBreadcrumbsModel { const versionIdThen = buffer.getVersionId(); const timeout = new TimeoutTimer(); - this._outlineDisposables.push({ + this._outlineDisposables.add({ dispose: () => { source.cancel(); source.dispose(); @@ -180,7 +187,7 @@ export class EditorBreadcrumbsModel { model = model.adopt(); this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - this._outlineDisposables.push(editor.onDidChangeCursorPosition(_ => { + this._outlineDisposables.add(editor.onDidChangeCursorPosition(_ => { timeout.cancelAndSet(() => { if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && editor.getModel()) { this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 5a56c61b3f039..b2e46d001d6b3 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -74,7 +74,7 @@ export interface IEditorOpeningEvent extends IEditorIdentifier { * Allows to prevent the opening of an editor by providing a callback * that will be executed instead. By returning another editor promise * it is possible to override the opening with another editor. It is ok - * to return a promise that resolves to NULL to prevent the opening + * to return a promise that resolves to `undefined` to prevent the opening * alltogether. */ prevent(callback: () => undefined | Promise): void; @@ -94,6 +94,7 @@ export interface IEditorGroupsAccessor { getGroups(order: GroupsOrder): IEditorGroupView[]; activateGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; + restoreGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): IEditorGroupView; mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView; diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 0464d3131be23..7b4cab33ed860 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -618,12 +618,24 @@ export abstract class BaseCloseAllAction extends Action { async run(): Promise { - // Just close all if there are no or one dirty editor - if (this.textFileService.getDirty().length < 2) { + // Just close all if there are no dirty editors + if (!this.textFileService.isDirty()) { return this.doCloseAll(); } - // Otherwise ask for combined confirmation + // Otherwise ask for combined confirmation and make sure + // to bring each dirty editor to the front so that the user + // can review if the files should be changed or not. + await Promise.all(this.groupsToClose.map(async groupToClose => { + for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (editor.isDirty()) { + return groupToClose.openEditor(editor); + } + } + + return undefined; + })); + const confirm = await this.textFileService.confirmSave(); if (confirm === ConfirmResult.CANCEL) { return; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index bbf73ff14da69..fb1e07119c70f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -481,7 +481,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private onDidEditorOpen(editor: EditorInput): void { - // Telemetry /* __GDPR__ "editorOpened" : { "${include}": [ @@ -520,14 +519,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } }); - // Telemetry /* __GDPR__ - "editorClosed" : { - "${include}": [ - "${EditorTelemetryDescriptor}" - ] - } - */ + "editorClosed" : { + "${include}": [ + "${EditorTelemetryDescriptor}" + ] + } + */ this.telemetryService.publicLog('editorClosed', this.toEditorTelemetryDescriptor(event.editor)); // Update container @@ -798,7 +796,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditor() - openEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options?: EditorOptions): Promise { // Guard against invalid inputs if (!editor) { @@ -814,7 +812,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Proceed with opening - return this.doOpenEditor(editor, options).then(withUndefinedAsNull); + return withUndefinedAsNull(await this.doOpenEditor(editor, options)); } private doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { @@ -833,12 +831,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView { openEditorOptions.active = true; } - // Set group active unless we open inactive or preserve focus - // Do this before we open the editor in the group to prevent a false - // active editor change event before the editor is loaded - // (see https://github.com/Microsoft/vscode/issues/51679) - if (openEditorOptions.active && (!options || !options.preserveFocus)) { - this.accessor.activateGroup(this); + if (openEditorOptions.active) { + // Set group active unless we are instructed to preserveFocus. Always + // make sure to restore a minimized group though in order to fix + // https://github.com/microsoft/vscode/issues/79633 + // + // Do this before we open the editor in the group to prevent a false + // active editor change event before the editor is loaded + // (see https://github.com/Microsoft/vscode/issues/51679) + if (options && options.preserveFocus) { + this.accessor.restoreGroup(this); + } else { + this.accessor.activateGroup(this); + } } // Actually move the editor if a specific index is provided and we figure diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 8bb14cc38a3c3..5607b641184eb 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -325,6 +325,13 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro return groupView; } + restoreGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { + const groupView = this.assertGroupView(group); + this.doRestoreGroup(groupView); + + return groupView; + } + getSize(group: IEditorGroupView | GroupIdentifier): { width: number, height: number } { const groupView = this.assertGroupView(group); @@ -337,7 +344,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this.gridWidget.resizeView(groupView, size); } - arrangeGroups(arrangement: GroupsArrangement): void { + arrangeGroups(arrangement: GroupsArrangement, target = this.activeGroup): void { if (this.count < 2) { return; // require at least 2 groups to show } @@ -351,10 +358,10 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this.gridWidget.distributeViewSizes(); break; case GroupsArrangement.MINIMIZE_OTHERS: - this.gridWidget.maximizeViewSize(this.activeGroup); + this.gridWidget.maximizeViewSize(target); break; case GroupsArrangement.TOGGLE: - if (this.isGroupMaximized(this.activeGroup)) { + if (this.isGroupMaximized(target)) { this.arrangeGroups(GroupsArrangement.EVEN); } else { this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); @@ -576,15 +583,19 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro group.setActive(true); // Maximize the group if it is currently minimized + this.doRestoreGroup(group); + + // Event + this._onDidActiveGroupChange.fire(group); + } + + private doRestoreGroup(group: IEditorGroupView): void { if (this.gridWidget) { const viewSize = this.gridWidget.getViewSize(group); if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) { - this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS, group); } } - - // Event - this._onDidActiveGroupChange.fire(group); } private doUpdateMostRecentActive(group: IEditorGroupView, makeMostRecentlyActive?: boolean): void { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index e1a7d9d0fecfa..0d54b781fb021 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -990,7 +990,7 @@ export class ChangeModeAction extends Action { private configureFileAssociation(resource: URI): void { const extension = extname(resource); const base = basename(resource); - const currentAssociation = this.modeService.getModeIdByFilepathOrFirstLine(resource.with({ path: base })); + const currentAssociation = this.modeService.getModeIdByFilepathOrFirstLine(URI.file(base)); const languages = this.modeService.getRegisteredLanguageNames(); const picks: IQuickPickItem[] = languages.sort().map((lang, index) => { diff --git a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css index d3850e293079c..1e6e29ee935be 100644 --- a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css @@ -17,6 +17,10 @@ text-decoration-line: underline; } +.monaco-workbench .monaco-breadcrumb-item.shows-symbol-icon .symbol-icon.block { + padding-right: 6px; +} + /* todo@joh move somewhere else */ .monaco-workbench .monaco-breadcrumbs-picker .arrow { diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsActions.css b/src/vs/workbench/browser/parts/notifications/media/notificationsActions.css index d5b23828af9ab..7f59f8471fb2f 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsActions.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsActions.css @@ -66,4 +66,4 @@ .vs .monaco-workbench > .notifications-center > .notifications-center-header .hide-all-notifications-action { background-image: url('tree-expanded-light.svg'); -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index f97e52b3507f8..719b6265b0054 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -91,6 +91,7 @@ .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-toolbar-container { display: none; + height: 22px; } .monaco-workbench .notifications-list-container .notification-list-item:hover .notification-list-item-toolbar-container, @@ -142,4 +143,4 @@ .monaco-workbench .notifications-list-container .progress-bit { height: 2px; bottom: 0; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts b/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts index 378d2be0c4c55..690543c695937 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts @@ -22,8 +22,8 @@ export function getIconClass(iconPath: { dark: URI; light?: URI; } | undefined): iconClass = iconPathToClass[key]; } else { iconClass = iconClassGenerator.nextId(); - dom.createCSSRule(`.${iconClass}`, `background-image: url("${dom.asDomUri(iconPath.light || iconPath.dark).toString()}")`); - dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: url("${dom.asDomUri(iconPath.dark).toString()}")`); + dom.createCSSRule(`.${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.light || iconPath.dark)}`); + dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.dark)}`); iconPathToClass[key] = iconClass; } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 66650e527f7cb..9e17bf39cc4bb 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -63,7 +63,7 @@ export class SidebarPart extends CompositePart implements IViewletServi return; } - return width; + return Math.max(width, 300); } //#endregion diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index bb5dda0c50f07..0a45e07062ed0 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IMenubarMenu, IMenubarMenuItemAction, IMenubarMenuItemSubmenu, IMenubarKeybinding, IMenubarService, IMenubarData, MenubarMenuItem } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWindowService, MenuBarVisibility, IWindowsService, getTitleBarStyle, IURIToOpen } from 'vs/platform/windows/common/windows'; @@ -33,7 +32,6 @@ import { attachMenuStyler } from 'vs/platform/theme/common/styler'; import { assign } from 'vs/base/common/objects'; import { mnemonicMenuLabel, unmnemonicLabel } from 'vs/base/common/labels'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { isFullscreen } from 'vs/base/browser/browser'; @@ -133,7 +131,7 @@ export abstract class MenubarControl extends Disposable { this.menuUpdater.schedule(); } - protected calculateActionLabel(action: IAction | IMenubarMenuItemAction): string { + protected calculateActionLabel(action: { id: string; label: string; }): string { let label = action.label; switch (action.id) { default: @@ -251,195 +249,6 @@ export abstract class MenubarControl extends Disposable { } } -export class NativeMenubarControl extends MenubarControl { - constructor( - @IMenuService menuService: IMenuService, - @IWindowService windowService: IWindowService, - @IWindowsService windowsService: IWindowsService, - @IContextKeyService contextKeyService: IContextKeyService, - @IKeybindingService keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService, - @ILabelService labelService: ILabelService, - @IUpdateService updateService: IUpdateService, - @IStorageService storageService: IStorageService, - @INotificationService notificationService: INotificationService, - @IPreferencesService preferencesService: IPreferencesService, - @IEnvironmentService environmentService: IEnvironmentService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IMenubarService private readonly menubarService: IMenubarService - ) { - super( - menuService, - windowService, - windowsService, - contextKeyService, - keybindingService, - configurationService, - labelService, - updateService, - storageService, - notificationService, - preferencesService, - environmentService, - accessibilityService); - - if (isMacintosh && !isWeb) { - this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService)); - this.topLevelTitles['Preferences'] = nls.localize('mPreferences', "Preferences"); - } - - for (const topLevelMenuName of Object.keys(this.topLevelTitles)) { - const menu = this.menus[topLevelMenuName]; - if (menu) { - this._register(menu.onDidChange(() => this.updateMenubar())); - } - } - - this.windowService.getRecentlyOpened().then((recentlyOpened) => { - this.recentlyOpened = recentlyOpened; - - this.doUpdateMenubar(true); - }); - - this.registerListeners(); - } - - protected doUpdateMenubar(firstTime: boolean): void { - // Send menus to main process to be rendered by Electron - const menubarData = { menus: {}, keybindings: {} }; - if (this.getMenubarMenus(menubarData)) { - this.menubarService.updateMenubar(this.windowService.windowId, menubarData); - } - } - - private getMenubarMenus(menubarData: IMenubarData): boolean { - if (!menubarData) { - return false; - } - - menubarData.keybindings = this.getAdditionalKeybindings(); - for (const topLevelMenuName of Object.keys(this.topLevelTitles)) { - const menu = this.menus[topLevelMenuName]; - if (menu) { - const menubarMenu: IMenubarMenu = { items: [] }; - this.populateMenuItems(menu, menubarMenu, menubarData.keybindings); - if (menubarMenu.items.length === 0) { - // Menus are incomplete - return false; - } - menubarData.menus[topLevelMenuName] = menubarMenu; - } - } - - return true; - } - - private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding | undefined }) { - let groups = menu.getActions(); - for (let group of groups) { - const [, actions] = group; - - actions.forEach(menuItem => { - - if (menuItem instanceof SubmenuItemAction) { - const submenu = { items: [] }; - - if (!this.menus[menuItem.item.submenu]) { - this.menus[menuItem.item.submenu] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService); - this._register(this.menus[menuItem.item.submenu]!.onDidChange(() => this.updateMenubar())); - } - - const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService); - this.populateMenuItems(menuToDispose, submenu, keybindings); - - let menubarSubmenuItem: IMenubarMenuItemSubmenu = { - id: menuItem.id, - label: menuItem.label, - submenu: submenu - }; - - menuToPopulate.items.push(menubarSubmenuItem); - menuToDispose.dispose(); - } else { - if (menuItem.id === 'workbench.action.openRecent') { - const actions = this.getOpenRecentActions().map(this.transformOpenRecentAction); - menuToPopulate.items.push(...actions); - } - - let menubarMenuItem: IMenubarMenuItemAction = { - id: menuItem.id, - label: menuItem.label - }; - - if (menuItem.checked) { - menubarMenuItem.checked = true; - } - - if (!menuItem.enabled) { - menubarMenuItem.enabled = false; - } - - menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem); - keybindings[menuItem.id] = this.getMenubarKeybinding(menuItem.id); - menuToPopulate.items.push(menubarMenuItem); - } - }); - - menuToPopulate.items.push({ id: 'vscode.menubar.separator' }); - } - - if (menuToPopulate.items.length > 0) { - menuToPopulate.items.pop(); - } - } - - private transformOpenRecentAction(action: Separator | (IAction & { uri: URI })): MenubarMenuItem { - if (action instanceof Separator) { - return { id: 'vscode.menubar.separator' }; - } - - return { - id: action.id, - uri: action.uri, - enabled: action.enabled, - label: action.label - }; - } - - private getAdditionalKeybindings(): { [id: string]: IMenubarKeybinding } { - const keybindings: { [id: string]: IMenubarKeybinding } = {}; - if (isMacintosh) { - const keybinding = this.getMenubarKeybinding('workbench.action.quit'); - if (keybinding) { - keybindings['workbench.action.quit'] = keybinding; - } - } - - return keybindings; - } - - private getMenubarKeybinding(id: string): IMenubarKeybinding | undefined { - const binding = this.keybindingService.lookupKeybinding(id); - if (!binding) { - return undefined; - } - - // first try to resolve a native accelerator - const electronAccelerator = binding.getElectronAccelerator(); - if (electronAccelerator) { - return { label: electronAccelerator, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) }; - } - - // we need this fallback to support keybindings that cannot show in electron menus (e.g. chords) - const acceleratorLabel = binding.getLabel(); - if (acceleratorLabel) { - return { label: acceleratorLabel, isNative: false, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) }; - } - - return undefined; - } -} - export class CustomMenubarControl extends MenubarControl { private menubar: MenuBar; private container: HTMLElement; diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 66bf29cdd22c3..fbf3423014fdc 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -27,7 +27,7 @@ import { URI } from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; import { EventType, EventHelper, Dimension, isAncestor, hide, show, removeClass, addClass, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; -import { MenubarControl, NativeMenubarControl, CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; +import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { template, getBaseLabel } from 'vs/base/common/labels'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -65,7 +65,7 @@ export class TitlebarPart extends Part implements ITitleService { private windowControls: HTMLElement; private maxRestoreControl: HTMLElement; private appIcon: HTMLElement; - private menubarPart: MenubarControl; + private customMenubar: CustomMenubarControl | undefined; private menubar: HTMLElement; private resizer: HTMLElement; private lastLayoutDimensions: Dimension; @@ -332,19 +332,16 @@ export class TitlebarPart extends Part implements ITitleService { }))); } - // Menubar: the menubar part which is responsible for populating both the custom and native menubars - if ((isMacintosh && !isWeb) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native') { - this.menubarPart = this.instantiationService.createInstance(NativeMenubarControl); - } else { - const customMenubarControl = this.instantiationService.createInstance(CustomMenubarControl); - this.menubarPart = customMenubarControl; + // Menubar: install a custom menu bar depending on configuration + if (getTitleBarStyle(this.configurationService, this.environmentService) !== 'native' && (!isMacintosh || isWeb)) { + this.customMenubar = this._register(this.instantiationService.createInstance(CustomMenubarControl)); this.menubar = append(this.element, $('div.menubar')); this.menubar.setAttribute('role', 'menubar'); - customMenubarControl.create(this.menubar); + this.customMenubar.create(this.menubar); - this._register(customMenubarControl.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); - this._register(customMenubarControl.onFocusStateChange(e => this.onMenubarFocusChanged(e))); + this._register(this.customMenubar.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); + this._register(this.customMenubar.onFocusStateChange(e => this.onMenubarFocusChanged(e))); } // Title @@ -547,7 +544,7 @@ export class TitlebarPart extends Part implements ITitleService { } private adjustTitleMarginToCenter(): void { - if (this.menubarPart instanceof CustomMenubarControl) { + if (this.customMenubar) { const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; const rightMarker = this.element.clientWidth - (this.windowControls ? this.windowControls.clientWidth : 0) - 10; @@ -588,9 +585,9 @@ export class TitlebarPart extends Part implements ITitleService { runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); - if (this.menubarPart instanceof CustomMenubarControl) { + if (this.customMenubar) { const menubarDimension = new Dimension(0, dimension.height); - this.menubarPart.layout(menubarDimension); + this.customMenubar.layout(menubarDimension); } } } diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 1163a29492076..2fcbf196e0d3a 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -69,7 +69,9 @@ export class CustomTreeViewPanel extends ViewletPanel { } renderBody(container: HTMLElement): void { - this.treeView.show(container); + if (this.treeView instanceof CustomTreeView) { + this.treeView.show(container); + } } layoutBody(height: number, width: number): void { @@ -161,12 +163,14 @@ export class CustomTreeView extends Disposable implements ITreeView { private _showCollapseAllAction = false; private focused: boolean = false; - private domNode: HTMLElement; - private treeContainer: HTMLElement; + private domNode!: HTMLElement; + private treeContainer!: HTMLElement; private _messageValue: string | undefined; - private messageElement: HTMLDivElement; - private tree: WorkbenchAsyncDataTree; - private treeLabels: ResourceLabels; + private _canSelectMany: boolean = false; + private messageElement!: HTMLDivElement; + private tree: WorkbenchAsyncDataTree | undefined; + private treeLabels: ResourceLabels | undefined; + private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; private menus: TitleMenus; @@ -218,12 +222,12 @@ export class CustomTreeView extends Disposable implements ITreeView { this.create(); } - private _dataProvider: ITreeViewDataProvider | null; - get dataProvider(): ITreeViewDataProvider | null { + private _dataProvider: ITreeViewDataProvider | undefined; + get dataProvider(): ITreeViewDataProvider | undefined { return this._dataProvider; } - set dataProvider(dataProvider: ITreeViewDataProvider | null) { + set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { if (dataProvider) { this._dataProvider = new class implements ITreeViewDataProvider { async getChildren(node: ITreeItem): Promise { @@ -238,7 +242,7 @@ export class CustomTreeView extends Disposable implements ITreeView { this.updateMessage(); this.refresh(); } else { - this._dataProvider = null; + this._dataProvider = undefined; this.updateMessage(); } } @@ -253,6 +257,14 @@ export class CustomTreeView extends Disposable implements ITreeView { this.updateMessage(); } + get canSelectMany(): boolean { + return this._canSelectMany; + } + + set canSelectMany(canSelectMany: boolean) { + this._canSelectMany = canSelectMany; + } + get hasIconForParentNode(): boolean { return this._hasIconForParentNode; } @@ -372,12 +384,14 @@ export class CustomTreeView extends Disposable implements ITreeView { collapseByDefault: (e: ITreeItem): boolean => { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; }, - multipleSelectionSupport: false - })); + multipleSelectionSupport: this.canSelectMany, + }) as WorkbenchAsyncDataTree); aligner.tree = this.tree; + const actionRunner = new MultipleSelectionActionRunner(() => this.tree!.getSelection()); + renderer.actionRunner = actionRunner; this.tree.contextKeyService.createKey(this.id, true); - this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e))); + this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); this._register(this.tree.onDidChangeCollapseState(e => { if (!e.node.element) { @@ -399,14 +413,14 @@ export class CustomTreeView extends Disposable implements ITreeView { if (!e.browserEvent) { return; } - const selection = this.tree.getSelection(); + const selection = this.tree!.getSelection(); if ((selection.length === 1) && selection[0].command) { this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || [])); } })); } - private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent): void { + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { const node: ITreeItem | null = treeEvent.element; if (node === null) { return; @@ -416,7 +430,7 @@ export class CustomTreeView extends Disposable implements ITreeView { event.preventDefault(); event.stopPropagation(); - this.tree.setFocus([node]); + this.tree!.setFocus([node]); const actions = treeMenus.getResourceContextActions(node); if (!actions.length) { return; @@ -436,13 +450,13 @@ export class CustomTreeView extends Disposable implements ITreeView { onHide: (wasCancelled?: boolean) => { if (wasCancelled) { - this.tree.domFocus(); + this.tree!.domFocus(); } }, getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), - actionRunner: new MultipleSelectionActionRunner(() => this.tree.getSelection()) + actionRunner }); } @@ -479,8 +493,8 @@ export class CustomTreeView extends Disposable implements ITreeView { DOM.clearNode(this.messageElement); } - private _height: number; - private _width: number; + private _height: number = 0; + private _width: number = 0; layout(height: number, width: number) { if (height && width) { this._height = height; @@ -532,10 +546,11 @@ export class CustomTreeView extends Disposable implements ITreeView { } async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { - if (this.tree) { + const tree = this.tree; + if (tree) { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; await Promise.all(itemOrItems.map(element => { - return this.tree.expand(element, false); + return tree.expand(element, false); })); } return Promise.resolve(undefined); @@ -575,18 +590,19 @@ export class CustomTreeView extends Disposable implements ITreeView { private refreshing: boolean = false; private async doRefresh(elements: ITreeItem[]): Promise { - if (this.tree) { + const tree = this.tree; + if (tree) { this.refreshing = true; const parents: Set = new Set(); elements.forEach(element => { if (element !== this.root) { - const parent = this.tree.getParentElement(element); + const parent = tree.getParentElement(element); parents.add(parent); } else { parents.add(element); } }); - await Promise.all(Array.from(parents.values()).map(element => this.tree.updateChildren(element, true))); + await Promise.all(Array.from(parents.values()).map(element => tree.updateChildren(element, true))); this.refreshing = false; this.updateContentAreas(); if (this.focused) { @@ -684,6 +700,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); + templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + if (this._actionRunner) { + templateData.actionBar.actionRunner = this._actionRunner; + } this.setAlignment(templateData.container, node); templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); } @@ -772,7 +797,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer; + private _tree: WorkbenchAsyncDataTree | undefined; constructor(private themeService: IWorkbenchThemeService) { super(); @@ -790,11 +815,15 @@ class Aligner extends Disposable { return false; } - const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); - if (this.hasIcon(parent)) { + if (this._tree) { + const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); + if (this.hasIcon(parent)) { + return false; + } + return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); + } else { return false; } - return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } private hasIcon(node: ITreeItem): boolean { @@ -816,23 +845,23 @@ class Aligner extends Disposable { class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: (() => any[])) { + constructor(private getSelectedResources: (() => ITreeItem[])) { super(); } - runAction(action: IAction, context: any): Promise { - if (action instanceof MenuItemAction) { - const selection = this.getSelectedResources(); - const filteredSelection = selection.filter(s => s !== context); - - if (selection.length === filteredSelection.length || selection.length === 1) { - return action.run(context); - } - - return action.run(context, ...filteredSelection); + runAction(action: IAction, context: TreeViewItemHandleArg): Promise { + const selection = this.getSelectedResources(); + let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; + if (selection.length > 1) { + selectionHandleArgs = []; + selection.forEach(selected => { + if (selected.handle !== context.$treeItemHandle) { + selectionHandleArgs!.push({ $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle }); + } + }); } - return super.runAction(action, context); + return action.run(...[context, selectionHandleArgs]); } } diff --git a/src/vs/workbench/browser/parts/views/media/panelviewlet.css b/src/vs/workbench/browser/parts/views/media/panelviewlet.css index e1ce2ee1b6067..e022f97beb884 100644 --- a/src/vs/workbench/browser/parts/views/media/panelviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/panelviewlet.css @@ -7,6 +7,10 @@ border-top: none !important; /* less clutter: do not show any border for first views in a panel */ } +.monaco-panel-view .panel > .panel-header > .actions.show { + display: initial; +} + .monaco-panel-view .panel > .panel-header h3.title { white-space: nowrap; text-overflow: ellipsis; diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index ef9dbe0d75ff2..4a5a2a0cbc335 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -41,6 +41,7 @@ export interface IViewletPanelOptions extends IPanelOptions { actionRunner?: IActionRunner; id: string; title: string; + showActionsAlways?: boolean; } export abstract class ViewletPanel extends Panel implements IView { @@ -67,6 +68,7 @@ export abstract class ViewletPanel extends Panel implements IView { protected actionRunner?: IActionRunner; protected toolbar: ToolBar; + private readonly showActionsAlways: boolean = false; private headerContainer: HTMLElement; private titleContainer: HTMLElement; @@ -82,6 +84,7 @@ export abstract class ViewletPanel extends Panel implements IView { this.id = options.id; this.title = options.title; this.actionRunner = options.actionRunner; + this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); } @@ -133,6 +136,7 @@ export abstract class ViewletPanel extends Panel implements IView { this.renderHeaderTitle(container, this.title); const actions = append(container, $('.actions')); + toggleClass(actions, 'show', this.showActionsAlways); this.toolbar = new ToolBar(actions, this.contextMenuService, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: action => this.getActionViewItem(action), diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 2983c1092501d..23accec76fa88 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -382,7 +382,19 @@ export class ContributableViewsModel extends Disposable { return 0; } - return (this.getViewOrder(a) - this.getViewOrder(b)) || (a.id < b.id ? -1 : 1); + return (this.getViewOrder(a) - this.getViewOrder(b)) || this.getGroupOrderResult(a, b) || (a.id < b.id ? -1 : 1); + } + + private getGroupOrderResult(a: IViewDescriptor, b: IViewDescriptor) { + if (!a.group || !b.group) { + return 0; + } + + if (a.group === b.group) { + return 0; + } + + return a.group < b.group ? -1 : 1; } private getViewOrder(viewDescriptor: IViewDescriptor): number { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 909f7578e4c0a..f5944ce974af1 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -6,20 +6,19 @@ import { mark } from 'vs/base/common/performance'; import { domContentLoaded, addDisposableListener, EventType, addClass } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; -import { SimpleLogService } from 'vs/workbench/browser/web.simpleservices'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { Workbench } from 'vs/workbench/browser/workbench'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME, RemoteExtensionsFileSystemProvider } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IProductService } from 'vs/platform/product/common/product'; +import { IProductService, IProductConfiguration } from 'vs/platform/product/common/product'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentServiceImpl'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IFileService, IFileSystemProvider } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -33,7 +32,6 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/browser/signService'; import { hash } from 'vs/base/common/hash'; import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; -import { ProductService } from 'vs/platform/product/browser/productService'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { BACKUPS } from 'vs/platform/environment/common/environment'; import { joinPath } from 'vs/base/common/resources'; @@ -41,6 +39,12 @@ import { BrowserStorageService } from 'vs/platform/storage/browser/storageServic import { IStorageService } from 'vs/platform/storage/common/storage'; import { getThemeTypeSelector, DARK, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; import { InMemoryUserDataProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider'; +import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; +import { BufferLogService } from 'vs/platform/log/common/bufferLog'; +import { FileLogService } from 'vs/platform/log/common/fileLogService'; +import { toLocalISOString } from 'vs/base/common/date'; +import { IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedDBLogProvider'; +import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLogProvider'; class CodeRendererMain extends Disposable { @@ -83,6 +87,11 @@ class CodeRendererMain extends Disposable { })); this._register(workbench.onShutdown(() => this.dispose())); + // Driver + if (this.configuration.driver) { + (async () => this._register(await registerWindowDriver()))(); + } + // Startup workbench.startup(); } @@ -111,22 +120,18 @@ class CodeRendererMain extends Disposable { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Log - const logService = new SimpleLogService(); + const logsPath = URI.file(toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')).with({ scheme: 'vscode-log' }); + const logService = new BufferLogService(); serviceCollection.set(ILogService, logService); - const payload = await this.resolveWorkspaceInitializationPayload(); + const payload = this.resolveWorkspaceInitializationPayload(); // Environment - const environmentService = new BrowserWorkbenchEnvironmentService({ - workspaceId: payload.id, - remoteAuthority: this.configuration.remoteAuthority, - webviewEndpoint: this.configuration.webviewEndpoint, - connectionToken: this.configuration.connectionToken - }); + const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: payload.id, logsPath, ...this.configuration }); serviceCollection.set(IWorkbenchEnvironmentService, environmentService); // Product - const productService = new ProductService(); + const productService = this.createProductService(); serviceCollection.set(IProductService, productService); // Remote @@ -138,36 +143,15 @@ class CodeRendererMain extends Disposable { serviceCollection.set(ISignService, signService); // Remote Agent - const remoteAgentService = this._register(new RemoteAgentService(this.configuration.webSocketFactory, environmentService, productService, remoteAuthorityResolverService, signService)); + const remoteAgentService = this._register(new RemoteAgentService(this.configuration.webSocketFactory, environmentService, productService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); // Files const fileService = this._register(new FileService(logService)); serviceCollection.set(IFileService, fileService); + this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, logService, logsPath); - let userDataProvider: IFileSystemProvider | undefined = this.configuration.userDataProvider; - const connection = remoteAgentService.getConnection(); - if (connection) { - const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); - const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); - - fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); - - if (!userDataProvider) { - const remoteUserDataUri = this.getRemoteUserDataUri(); - if (remoteUserDataUri) { - userDataProvider = this._register(new FileUserDataProvider(remoteUserDataUri, joinPath(remoteUserDataUri, BACKUPS), remoteFileSystemProvider, environmentService)); - } - } - } - - if (!userDataProvider) { - userDataProvider = this._register(new InMemoryUserDataProvider()); - } - - // User Data Provider - fileService.registerProvider(Schemas.userData, userDataProvider); - + // Long running services (workspace, config, storage) const services = await Promise.all([ this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, logService).then(service => { @@ -192,6 +176,62 @@ class CodeRendererMain extends Disposable { return { serviceCollection, logService, storageService: services[1] }; } + private registerFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IFileService, remoteAgentService: IRemoteAgentService, logService: BufferLogService, logsPath: URI): void { + + // Logger + const indexedDBLogProvider = new IndexedDBLogProvider(logsPath.scheme); + (async () => { + try { + await indexedDBLogProvider.database; + + fileService.registerProvider(logsPath.scheme, indexedDBLogProvider); + } catch (error) { + (logService).info('Error while creating indexedDB log provider. Falling back to in-memory log provider.'); + (logService).error(error); + + fileService.registerProvider(logsPath.scheme, new InMemoryLogProvider(logsPath.scheme)); + } + + const consoleLogService = new ConsoleLogService(logService.getLevel()); + const fileLogService = new FileLogService('window', environmentService.logFile, logService.getLevel(), fileService); + logService.logger = new MultiplexLogService([consoleLogService, fileLogService]); + })(); + + const connection = remoteAgentService.getConnection(); + if (connection) { + + // Remote file system + const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); + const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); + fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); + + if (!this.configuration.userDataProvider) { + const remoteUserDataUri = this.getRemoteUserDataUri(); + if (remoteUserDataUri) { + this.configuration.userDataProvider = this._register(new FileUserDataProvider(remoteUserDataUri, joinPath(remoteUserDataUri, BACKUPS), remoteFileSystemProvider, environmentService)); + } + } + } + + // User data + if (!this.configuration.userDataProvider) { + this.configuration.userDataProvider = this._register(new InMemoryUserDataProvider()); + } + fileService.registerProvider(Schemas.userData, this.configuration.userDataProvider); + } + + private createProductService(): IProductService { + const productConfiguration = { + ...this.configuration.productConfiguration ? this.configuration.productConfiguration : { + version: '1.38.0-unknown', + nameLong: 'Unknown', + extensionAllowedProposedApi: [], + }, ...{ urlProtocol: '' } + } as IProductConfiguration; + + return { _serviceBrand: undefined, ...productConfiguration }; + } + private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { const storageService = new BrowserStorageService(environmentService, fileService); @@ -245,6 +285,7 @@ class CodeRendererMain extends Disposable { return joinPath(URI.revive(JSON.parse(remoteUserDataPath)), 'User'); } } + return null; } } diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts index e31a873b221a0..895a8a0393f14 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -7,13 +7,8 @@ import { URI } from 'vs/base/common/uri'; import * as browser from 'vs/base/browser/browser'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event } from 'vs/base/common/event'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IGalleryExtension, IExtensionIdentifier, IReportedExtension, IExtensionManagementService, ILocalExtension, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionTipsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionType, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IURLHandler, IURLService } from 'vs/platform/url/common/url'; -import { ConsoleLogService, ILogService } from 'vs/platform/log/common/log'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IURIToOpen, IMessageBoxResult, IWindowsService, IOpenSettings, IWindowSettings } from 'vs/platform/windows/common/windows'; @@ -22,9 +17,8 @@ import { IRecentlyOpened, IRecent, isRecentFile, isRecentFolder } from 'vs/platf import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -// tslint:disable-next-line: import-patterns -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { addDisposableListener, EventType, windowOpenNoOpener } from 'vs/base/browser/dom'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { pathsToEditors } from 'vs/workbench/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; @@ -32,139 +26,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { toStoreData, restoreRecentlyOpened } from 'vs/platform/history/common/historyStorage'; -// tslint:disable-next-line: import-patterns -import { IExperimentService, IExperiment, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProductService } from 'vs/platform/product/common/product'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; - -//#region Extension Tips - -export class SimpleExtensionTipsService implements IExtensionTipsService { - _serviceBrand: any; - - onRecommendationChange = Event.None; - - getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string; }; } { - return Object.create(null); - } - - getFileBasedRecommendations(): IExtensionRecommendation[] { - return []; - } - - getOtherRecommendations(): Promise { - return Promise.resolve([]); - } - - getWorkspaceRecommendations(): Promise { - return Promise.resolve([]); - } - - getKeymapRecommendations(): IExtensionRecommendation[] { - return []; - } - - toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void { - } - - getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } { - return { global: [], workspace: [] }; - } -} - -registerSingleton(IExtensionTipsService, SimpleExtensionTipsService, true); - -//#endregion - -export class SimpleExtensionManagementService implements IExtensionManagementService { - - _serviceBrand: any; - - onInstallExtension = Event.None; - onDidInstallExtension = Event.None; - onUninstallExtension = Event.None; - onDidUninstallExtension = Event.None; - - zip(extension: ILocalExtension): Promise { - // @ts-ignore - return Promise.resolve(undefined); - } - - unzip(zipLocation: URI, type: ExtensionType): Promise { - // @ts-ignore - return Promise.resolve(undefined); - } - - install(vsix: URI): Promise { - // @ts-ignore - return Promise.resolve(undefined); - } - - installFromGallery(extension: IGalleryExtension): Promise { - // @ts-ignore - return Promise.resolve(undefined); - } - - uninstall(extension: ILocalExtension, force?: boolean): Promise { - return Promise.resolve(undefined); - } - - reinstallFromGallery(extension: ILocalExtension): Promise { - return Promise.resolve(undefined); - } - - getInstalled(type?: ExtensionType): Promise { - // @ts-ignore - return Promise.resolve([]); - } - - getExtensionsReport(): Promise { - // @ts-ignore - return Promise.resolve([]); - } - - updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { - // @ts-ignore - return Promise.resolve(local); - } -} - -registerSingleton(IExtensionManagementService, SimpleExtensionManagementService); - -//#endregion - -//#region Extension URL Handler - -export const IExtensionUrlHandler = createDecorator('inactiveExtensionUrlHandler'); - -export interface IExtensionUrlHandler { - readonly _serviceBrand: any; - registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IURLHandler): void; - unregisterExtensionHandler(extensionId: ExtensionIdentifier): void; -} - -export class SimpleExtensionURLHandler implements IExtensionUrlHandler { - - _serviceBrand: any; - - registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IURLHandler): void { } - - unregisterExtensionHandler(extensionId: ExtensionIdentifier): void { } -} - -registerSingleton(IExtensionUrlHandler, SimpleExtensionURLHandler, true); - -//#endregion - -//#region Log - -export class SimpleLogService extends ConsoleLogService { } - -//#endregion +// tslint:disable-next-line: import-patterns +import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/common/workspaceStats'; //#region Update @@ -200,24 +68,6 @@ registerSingleton(IUpdateService, SimpleUpdateService); //#endregion -//#region URL - -export class SimpleURLService implements IURLService { - _serviceBrand: any; - - open(url: URI): Promise { - return Promise.resolve(false); - } - - registerHandler(handler: IURLHandler): IDisposable { - return Disposable.None; - } -} - -registerSingleton(IURLService, SimpleURLService); - -//#endregion - //#region Window export class SimpleWindowService extends Disposable implements IWindowService { @@ -240,7 +90,6 @@ export class SimpleWindowService extends Disposable implements IWindowService { @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @ILogService private readonly logService: ILogService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { super(); @@ -436,7 +285,7 @@ export class SimpleWindowService extends Disposable implements IWindowService { for (let i = 0; i < _uris.length; i++) { const uri = _uris[i]; if ('folderUri' in uri) { - const newAddress = `${document.location.origin}/?folder=${uri.folderUri.path}${this.workbenchEnvironmentService.configuration.connectionToken ? `&tkn=${this.workbenchEnvironmentService.configuration.connectionToken}` : ''}`; + const newAddress = `${document.location.origin}/?folder=${uri.folderUri.path}`; if (openFolderInNewWindow) { window.open(newAddress); } else { @@ -470,6 +319,8 @@ export class SimpleWindowService extends Disposable implements IWindowService { } closeWindow(): Promise { + window.close(); + return Promise.resolve(); } @@ -521,7 +372,6 @@ export class SimpleWindowsService implements IWindowsService { readonly onRecentlyOpenedChange: Event = Event.None; constructor( - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IDialogService private readonly dialogService: IDialogService, @IProductService private readonly productService: IProductService, @IClipboardService private readonly clipboardService: IClipboardService @@ -713,11 +563,6 @@ export class SimpleWindowsService implements IWindowsService { addQueryParameter('ibe', ibe); } - // add connection token - if (this.workbenchEnvironmentService.configuration.connectionToken) { - addQueryParameter('tkn', this.workbenchEnvironmentService.configuration.connectionToken); - } - window.open(newAddress); return Promise.resolve(); @@ -774,6 +619,8 @@ export class SimpleWindowsService implements IWindowsService { // This needs to be handled from browser process to prevent // foreground ordering issues on Windows openExternal(_url: string): Promise { + windowOpenNoOpener(_url); + return Promise.resolve(true); } @@ -901,33 +748,26 @@ registerSingleton(ITunnelService, SimpleTunnelService); //#endregion -//#region experiments +//#region workspace stats + +class SimpleWorkspaceStatsService implements IWorkspaceStatsService { -class ExperimentService implements IExperimentService { _serviceBrand: any; - async getExperimentById(id: string): Promise { - return { - enabled: false, - id: '', - state: ExperimentState.NoRun - }; + getTags(): Promise { + return Promise.resolve({}); } - async getExperimentsByType(type: ExperimentActionType): Promise { - return []; + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined { + return undefined; } - async getCuratedExtensionsList(curatedExtensionsKey: string): Promise { - return []; + getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise { + return Promise.resolve([]); } - markAsCompleted(experimentId: string): void { } - - onExperimentEnabled: Event = Event.None; - } -registerSingleton(IExperimentService, ExperimentService); +registerSingleton(IWorkspaceStatsService, SimpleWorkspaceStatsService); //#endregion diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 7c36ea51726f7..5504ef503070f 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -237,8 +237,13 @@ import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform' 'workbench.useExperimentalGridLayout': { 'type': 'boolean', 'description': nls.localize('workbench.useExperimentalGridLayout', "Enables the grid layout for the workbench. This setting may enable additional layout options for workbench components."), - 'default': false, + 'default': true, 'scope': ConfigurationScope.APPLICATION + }, + 'workbench.octiconsUpdate.enabled': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('workbench.octiconsUpdate.enabled', "Controls the visibility of the new Octicons style in the workbench.") } } }); @@ -246,7 +251,7 @@ import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform' // Window let windowTitleDescription = nls.localize('windowTitle', "Controls the window title based on the active editor. Variables are substituted based on the context:"); - windowTitleDescription += [ + windowTitleDescription += '\n- ' + [ nls.localize('activeEditorShort', "`\${activeEditorShort}`: the file name (e.g. myFile.txt)."), nls.localize('activeEditorMedium', "`\${activeEditorMedium}`: the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt)."), nls.localize('activeEditorLong', "`\${activeEditorLong}`: the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt)."), diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 0e61b26920de0..2506a50c8c395 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -50,12 +50,12 @@ export class Workbench extends Layout { private readonly _onBeforeShutdown = this._register(new Emitter()); readonly onBeforeShutdown: Event = this._onBeforeShutdown.event; - private readonly _onShutdown = this._register(new Emitter()); - readonly onShutdown: Event = this._onShutdown.event; - private readonly _onWillShutdown = this._register(new Emitter()); readonly onWillShutdown: Event = this._onWillShutdown.event; + private readonly _onShutdown = this._register(new Emitter()); + readonly onShutdown: Event = this._onShutdown.event; + constructor( parent: HTMLElement, private readonly serviceCollection: ServiceCollection, @@ -429,7 +429,7 @@ export class Workbench extends Layout { // Restore Editor Center Mode if (this.state.editor.restoreCentered) { - this.centerEditorLayout(true); + this.centerEditorLayout(true, true); } // Emit a warning after 10s if restore does not complete diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 561b5cb6f87f0..1a0af06b7a70b 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -7,7 +7,6 @@ import { URI } from 'vs/base/common/uri'; import { suggestFilename } from 'vs/base/common/mime'; import { memoize } from 'vs/base/common/decorators'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { basename } from 'vs/base/common/path'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult, Verbosity, IModeSupport } from 'vs/workbench/common/editor'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; @@ -64,7 +63,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport @memoize private get shortDescription(): string { - return basename(this.labelService.getUriLabel(dirname(this.resource))); + return this.labelService.getUriBasenameLabel(dirname(this.resource)); } @memoize diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index c9ec1a841f638..7306d530fcb5c 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -51,7 +51,7 @@ export interface IViewContainersRegistry { * * @returns the registered ViewContainer. */ - registerViewContainer(id: string, hideIfEmpty?: boolean, extensionId?: ExtensionIdentifier): ViewContainer; + registerViewContainer(id: string, hideIfEmpty?: boolean, extensionId?: ExtensionIdentifier, viewOrderDelegate?: ViewOrderDelegate): ViewContainer; /** * Deregisters the given view container @@ -67,8 +67,12 @@ export interface IViewContainersRegistry { get(id: string): ViewContainer | undefined; } +interface ViewOrderDelegate { + getOrder(group?: string): number | undefined; +} + export class ViewContainer { - protected constructor(readonly id: string, readonly hideIfEmpty: boolean, readonly extensionId?: ExtensionIdentifier) { } + protected constructor(readonly id: string, readonly hideIfEmpty: boolean, readonly extensionId?: ExtensionIdentifier, readonly orderDelegate?: ViewOrderDelegate) { } } class ViewContainersRegistryImpl extends Disposable implements IViewContainersRegistry { @@ -85,7 +89,7 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe return values(this.viewContainers); } - registerViewContainer(id: string, hideIfEmpty?: boolean, extensionId?: ExtensionIdentifier): ViewContainer { + registerViewContainer(id: string, hideIfEmpty?: boolean, extensionId?: ExtensionIdentifier, viewOrderDelegate?: ViewOrderDelegate): ViewContainer { const existing = this.viewContainers.get(id); if (existing) { return existing; @@ -93,7 +97,7 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe const viewContainer = new class extends ViewContainer { constructor() { - super(id, !!hideIfEmpty, extensionId); + super(id, !!hideIfEmpty, extensionId, viewOrderDelegate); } }; this.viewContainers.set(id, viewContainer); @@ -126,6 +130,8 @@ export interface IViewDescriptor { readonly when?: ContextKeyExpr; + readonly group?: string; + readonly order?: number; readonly weight?: number; @@ -300,10 +306,12 @@ export interface IViewsService { export interface ITreeView extends IDisposable { - dataProvider: ITreeViewDataProvider | null; + dataProvider: ITreeViewDataProvider | undefined; showCollapseAllAction: boolean; + canSelectMany: boolean; + message?: string; readonly visible: boolean; @@ -326,8 +334,6 @@ export interface ITreeView extends IDisposable { layout(height: number, width: number): void; - show(container: HTMLElement): void; - getOptimalWidth(): number; reveal(item: ITreeItem): Promise; diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 9d6b5c95ba262..a45ab6500316c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -7,7 +7,7 @@ import 'vs/css!./accessibility'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { renderFormattedText } from 'vs/base/browser/htmlContentRenderer'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Widget } from 'vs/base/browser/ui/widget'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 1270b1422286b..f036bfb8c58c5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom'; import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; import { Widget } from 'vs/base/browser/ui/widget'; import { Delayer } from 'vs/base/common/async'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { SimpleButton } from 'vs/editor/contrib/find/findWidget'; @@ -29,20 +29,20 @@ export abstract class SimpleFindWidget extends Widget { private readonly _findInput: FindInput; private readonly _domNode: HTMLElement; private readonly _innerDomNode: HTMLElement; - private _isVisible: boolean = false; private readonly _focusTracker: dom.IFocusTracker; private readonly _findInputFocusTracker: dom.IFocusTracker; private readonly _updateHistoryDelayer: Delayer; - private prevBtn: SimpleButton; - private nextBtn: SimpleButton; - private foundMatch: boolean; + private readonly prevBtn: SimpleButton; + private readonly nextBtn: SimpleButton; + + private _isVisible: boolean = false; + private foundMatch: boolean = false; constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, private readonly _state: FindReplaceState = new FindReplaceState(), - showOptionButtons?: boolean, - private readonly _invertDefaultDirection: boolean = false + showOptionButtons?: boolean ) { super(); @@ -93,20 +93,6 @@ export abstract class SimpleFindWidget extends Widget { this.findFirst(); })); - this._register(this._findInput.onKeyDown((e) => { - if (e.equals(KeyCode.Enter)) { - this.find(this._invertDefaultDirection); - e.preventDefault(); - return; - } - - if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - this.find(!this._invertDefaultDirection); - e.preventDefault(); - return; - } - })); - this.prevBtn = this._register(new SimpleButton({ label: NLS_PREVIOUS_MATCH_BTN_LABEL, className: 'previous', diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 7ab435e96ca24..5baf982dd9383 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 72de87a6f1cd6..e1f0c4496a0dc 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -8,7 +8,7 @@ import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; import { IAction, Action } from 'vs/base/common/actions'; import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, EDITOR_CONTRIBUTION_ID, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; -import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { AddFunctionBreakpointAction, ToggleBreakpointsActivatedAction, RemoveAllBreakpointsAction, RemoveBreakpointAction, EnableAllBreakpointsAction, DisableAllBreakpointsAction, ReapplyBreakpointsAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -74,6 +74,7 @@ export class BreakpointsView extends ViewletPanel { this.instantiationService.createInstance(BreakpointsRenderer), new ExceptionBreakpointsRenderer(this.debugService), this.instantiationService.createInstance(FunctionBreakpointsRenderer), + this.instantiationService.createInstance(DataBreakpointsRenderer), new FunctionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService) ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, @@ -91,7 +92,7 @@ export class BreakpointsView extends ViewletPanel { this._register(this.list.onContextMenu(this.onListContextMenu, this)); - this._register(this.list.onDidOpen(e => { + this._register(this.list.onDidOpen(async e => { let isSingleClick = false; let isDoubleClick = false; let isMiddleClick = false; @@ -110,9 +111,11 @@ export class BreakpointsView extends ViewletPanel { if (isMiddleClick) { if (element instanceof Breakpoint) { - this.debugService.removeBreakpoints(element.getId()); + await this.debugService.removeBreakpoints(element.getId()); } else if (element instanceof FunctionBreakpoint) { - this.debugService.removeFunctionBreakpoints(element.getId()); + await this.debugService.removeFunctionBreakpoints(element.getId()); + } else if (element instanceof DataBreakpoint) { + await this.debugService.removeDataBreakpoints(element.getId()); } return; } @@ -222,14 +225,14 @@ export class BreakpointsView extends ViewletPanel { private get elements(): IEnablement[] { const model = this.debugService.getModel(); - const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getBreakpoints()); + const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()); return elements; } private getExpandedBodySize(): number { const model = this.debugService.getModel(); - const length = model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length; + const length = model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length + model.getDataBreakpoints().length; return Math.min(BreakpointsView.MAX_VISIBLE_FILES, length) * 22; } } @@ -259,6 +262,9 @@ class BreakpointsDelegate implements IListVirtualDelegate { if (element instanceof ExceptionBreakpoint) { return ExceptionBreakpointsRenderer.ID; } + if (element instanceof DataBreakpoint) { + return DataBreakpointsRenderer.ID; + } return ''; } @@ -454,6 +460,61 @@ class FunctionBreakpointsRenderer implements IListRenderer { + + constructor( + @IDebugService private readonly debugService: IDebugService + ) { + // noop + } + + static readonly ID = 'databreakpoints'; + + get templateId() { + return DataBreakpointsRenderer.ID; + } + + renderTemplate(container: HTMLElement): IBaseBreakpointWithIconTemplateData { + const data: IBreakpointTemplateData = Object.create(null); + data.breakpoint = dom.append(container, $('.breakpoint')); + + data.icon = $('.icon'); + data.checkbox = createCheckbox(); + data.toDispose = []; + data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { + this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); + })); + + dom.append(data.breakpoint, data.icon); + dom.append(data.breakpoint, data.checkbox); + + data.name = dom.append(data.breakpoint, $('span.name')); + + return data; + } + + renderElement(dataBreakpoint: DataBreakpoint, index: number, data: IBaseBreakpointWithIconTemplateData): void { + data.context = dataBreakpoint; + data.name.textContent = dataBreakpoint.label; + const { className, message } = getBreakpointMessageAndClassName(this.debugService, dataBreakpoint); + data.icon.className = className + ' icon'; + data.icon.title = message ? message : ''; + data.checkbox.checked = dataBreakpoint.enabled; + data.breakpoint.title = dataBreakpoint.label; + + // Mark function breakpoints as disabled if deactivated or if debug type does not support them #9099 + const session = this.debugService.getViewModel().focusedSession; + dom.toggleClass(data.breakpoint, 'disabled', (session && !session.capabilities.supportsDataBreakpoints) || !this.debugService.getModel().areBreakpointsActivated()); + if (session && !session.capabilities.supportsDataBreakpoints) { + data.breakpoint.title = nls.localize('dataBreakpointsNotSupported', "Data breakpoints are not supported by this debug type"); + } + } + + disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { + dispose(templateData.toDispose); + } +} + class FunctionBreakpointInputRenderer implements IListRenderer { constructor( @@ -543,9 +604,9 @@ class FunctionBreakpointInputRenderer implements IListRenderer { +export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolean, preserveFocus: boolean, debugService: IDebugService, editorService: IEditorService): Promise { if (breakpoint.uri.scheme === DEBUG_SCHEME && debugService.state === State.Inactive) { - return Promise.resolve(null); + return Promise.resolve(undefined); } const selection = breakpoint.endLineNumber ? { @@ -572,7 +633,7 @@ export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolea }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } -export function getBreakpointMessageAndClassName(debugService: IDebugService, breakpoint: IBreakpoint | FunctionBreakpoint): { message?: string, className: string } { +export function getBreakpointMessageAndClassName(debugService: IDebugService, breakpoint: IBreakpoint | FunctionBreakpoint | DataBreakpoint): { message?: string, className: string } { const state = debugService.state; const debugActive = state === State.Running || state === State.Stopped; @@ -584,7 +645,7 @@ export function getBreakpointMessageAndClassName(debugService: IDebugService, br } const appendMessage = (text: string): string => { - return !(breakpoint instanceof FunctionBreakpoint) && breakpoint.message ? text.concat(', ' + breakpoint.message) : text; + return !(breakpoint instanceof FunctionBreakpoint) && !(breakpoint instanceof DataBreakpoint) && breakpoint.message ? text.concat(', ' + breakpoint.message) : text; }; if (debugActive && !breakpoint.verified) { return { @@ -607,6 +668,19 @@ export function getBreakpointMessageAndClassName(debugService: IDebugService, br }; } + if (breakpoint instanceof DataBreakpoint) { + if (session && !session.capabilities.supportsDataBreakpoints) { + return { + className: 'debug-data-breakpoint-unverified', + message: nls.localize('dataBreakpointUnsupported', "Data breakpoints not supported by this debug type"), + }; + } + + return { + className: 'debug-data-breakpoint', + }; + } + if (breakpoint.logMessage || breakpoint.condition || breakpoint.hitCondition) { const messages: string[] = []; if (breakpoint.logMessage) { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 93a7a5ed2bd0b..46e90870f22b6 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -144,7 +144,8 @@ export class CallStackView extends ViewletPanel { return nls.localize('showMoreStackFrames2', "Show More Stack Frames"); } - } + }, + expandOnlyOnTwistieClick: true }); this.tree.setInput(this.debugService.getModel()).then(undefined, onUnexpectedError); @@ -576,7 +577,7 @@ function isDebugModel(obj: any): obj is IDebugModel { } function isDebugSession(obj: any): obj is IDebugSession { - return typeof obj.getAllThreads === 'function'; + return obj && typeof obj.getAllThreads === 'function'; } function isDeemphasized(frame: IStackFrame): boolean { @@ -589,16 +590,21 @@ class CallStackDataSource implements IAsyncDataSource 1) || (threads.length === 1 && threads[0].stopped) || (this.debugService.getModel().getSessions().filter(s => s.parentSession === element).length > 0); + } + + return isDebugModel(element) || (element instanceof Thread && element.stopped); } - getChildren(element: IDebugModel | CallStackItem): Promise { + async getChildren(element: IDebugModel | CallStackItem): Promise { if (isDebugModel(element)) { const sessions = element.getSessions(); if (sessions.length === 0) { return Promise.resolve([]); } - if (sessions.length > 1 || this.debugService.getViewModel().isMultiSessionView()) { + if (sessions.length > 1) { return Promise.resolve(sessions.filter(s => !s.parentSession)); } @@ -607,11 +613,14 @@ class CallStackDataSource implements IAsyncDataSourcethreads[0]) : Promise.resolve(threads); } else if (isDebugSession(element)) { const childSessions = this.debugService.getModel().getSessions().filter(s => s.parentSession === element); - if (childSessions.length) { - return Promise.resolve(childSessions); + const threads: CallStackItem[] = element.getAllThreads(); + if (threads.length === 1) { + // Do not show thread when there is only one to be compact. + const children = await this.getThreadChildren(threads[0]); + return children.concat(childSessions); } - return Promise.resolve(element.getAllThreads()); + return Promise.resolve(threads.concat(childSessions)); } else { return this.getThreadChildren(element); } diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts index f2ed8a479ac6b..8f926b2a35a3c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActions.ts @@ -9,7 +9,7 @@ import * as lifecycle from 'vs/base/common/lifecycle'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Variable, Breakpoint, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -191,7 +191,7 @@ export class RemoveBreakpointAction extends AbstractDebugAction { public run(breakpoint: IBreakpoint): Promise { return breakpoint instanceof Breakpoint ? this.debugService.removeBreakpoints(breakpoint.getId()) - : this.debugService.removeFunctionBreakpoints(breakpoint.getId()); + : breakpoint instanceof FunctionBreakpoint ? this.debugService.removeFunctionBreakpoints(breakpoint.getId()) : this.debugService.removeDataBreakpoints(breakpoint.getId()); } } @@ -205,7 +205,7 @@ export class RemoveAllBreakpointsAction extends AbstractDebugAction { } public run(): Promise { - return Promise.all([this.debugService.removeBreakpoints(), this.debugService.removeFunctionBreakpoints()]); + return Promise.all([this.debugService.removeBreakpoints(), this.debugService.removeFunctionBreakpoints(), this.debugService.removeDataBreakpoints()]); } protected isEnabled(state: State): boolean { diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 67bcdb80de94e..7acca069182d0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -10,7 +10,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, REPL_ID, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; -import { Expression, Variable, Breakpoint, FunctionBreakpoint, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Expression, Variable, Breakpoint, FunctionBreakpoint, Thread, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -410,6 +410,8 @@ export function registerCommands(): void { debugService.removeBreakpoints(element.getId()); } else if (element instanceof FunctionBreakpoint) { debugService.removeFunctionBreakpoints(element.getId()); + } else if (element instanceof DataBreakpoint) { + debugService.removeDataBreakpoints(element.getId()); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 637086088b3c6..3e71419c14a59 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -35,6 +35,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { onUnexpectedError } from 'vs/base/common/errors'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { withUndefinedAsNull } from 'vs/base/common/types'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); jsonRegistry.registerSchema(launchSchemaId, launchSchema); @@ -577,7 +578,7 @@ class Launch extends AbstractLaunch implements ILaunch { pinned: created, revealIfVisible: true }, - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor, created }))); + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor: withUndefinedAsNull(editor), created }))); }, (error: Error) => { throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error.message)); }); @@ -613,7 +614,7 @@ class WorkspaceLaunch extends AbstractLaunch implements ILaunch { return this.editorService.openEditor({ resource: this.contextService.getWorkspace().configuration!, options: { preserveFocus } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor, created: false })); + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor: withUndefinedAsNull(editor), created: false })); } } @@ -647,6 +648,6 @@ class UserLaunch extends AbstractLaunch implements ILaunch { } openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string): Promise<{ editor: IEditor | null, created: boolean }> { - return this.preferencesService.openGlobalSettings(false, { preserveFocus }).then(editor => ({ editor, created: false })); + return this.preferencesService.openGlobalSettings(false, { preserveFocus }).then(editor => ({ editor: withUndefinedAsNull(editor), created: false })); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 060f919707434..b7f89249ad911 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -35,19 +35,23 @@ class ToggleBreakpointAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const debugService = accessor.get(IDebugService); - - const position = editor.getPosition(); - if (editor.hasModel() && position) { + if (editor.hasModel()) { + const debugService = accessor.get(IDebugService); const modelUri = editor.getModel().uri; - const bps = debugService.getModel().getBreakpoints({ lineNumber: position.lineNumber, uri: modelUri }); - - if (bps.length) { - return Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); - } - if (debugService.getConfigurationManager().canSetBreakpointsIn(editor.getModel())) { - return debugService.addBreakpoints(modelUri, [{ lineNumber: position.lineNumber }], 'debugEditorActions.toggleBreakpointAction'); - } + const canSet = debugService.getConfigurationManager().canSetBreakpointsIn(editor.getModel()); + // Does not account for multi line selections, Set to remove multiple cursor on the same line + const lineNumbers = [...new Set(editor.getSelections().map(s => s.getPosition().lineNumber))]; + + return Promise.all(lineNumbers.map(line => { + const bps = debugService.getModel().getBreakpoints({ lineNumber: line, uri: modelUri }); + if (bps.length) { + return Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); + } else if (canSet) { + return (debugService.addBreakpoints(modelUri, [{ lineNumber: line }], 'debugEditorActions.toggleBreakpointAction')); + } else { + return Promise.resolve([]); + } + })); } return Promise.resolve(); @@ -165,7 +169,7 @@ class SelectionToReplAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const debugService = accessor.get(IDebugService); const panelService = accessor.get(IPanelService); const viewModel = debugService.getViewModel(); @@ -175,9 +179,8 @@ class SelectionToReplAction extends EditorAction { } const text = editor.getModel().getValueInRange(editor.getSelection()); - return session.addReplExpression(viewModel.focusedStackFrame!, text) - .then(() => panelService.openPanel(REPL_ID, true)) - .then(_ => undefined); + await session.addReplExpression(viewModel.focusedStackFrame!, text); + await panelService.openPanel(REPL_ID, true); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index f06919be1d914..b7f66a4a69bf4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -245,6 +245,9 @@ export class DebugHoverWidget implements IContentWidget { this.layoutTreeAndContainer(); this.editor.layoutContentWidget(this); this.scrollbar.scanDomNode(); + this.tree.scrollTop = 0; + this.tree.scrollLeft = 0; + if (focus) { this.editor.render(); this.tree.domFocus(); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index eddc8876af727..86c6bb089b607 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -18,7 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { DebugModel, ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DebugModel, ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, Expression, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; import * as debugactions from 'vs/workbench/contrib/debug/browser/debugActions'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; @@ -52,6 +52,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; +const DEBUG_DATA_BREAKPOINTS_KEY = 'debug.databreakpoint'; const DEBUG_EXCEPTION_BREAKPOINTS_KEY = 'debug.exceptionbreakpoint'; const DEBUG_WATCH_EXPRESSIONS_KEY = 'debug.watchexpressions'; @@ -129,7 +130,7 @@ export class DebugService implements IDebugService { this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.model = new DebugModel(this.loadBreakpoints(), this.storageService.getBoolean(DEBUG_BREAKPOINTS_ACTIVATED_KEY, StorageScope.WORKSPACE, true), this.loadFunctionBreakpoints(), - this.loadExceptionBreakpoints(), this.loadWatchExpressions(), this.textFileService); + this.loadExceptionBreakpoints(), this.loadDataBreakpoints(), this.loadWatchExpressions(), this.textFileService); this.toDispose.push(this.model); this.viewModel = new ViewModel(contextKeyService); @@ -403,7 +404,7 @@ export class DebugService implements IDebugService { .then(() => false); } - return launch && launch.openConfigFile(false, true, undefined, this.initCancellationToken ? this.initCancellationToken.token : undefined).then(() => false); + return !!launch && launch.openConfigFile(false, true, undefined, this.initCancellationToken ? this.initCancellationToken.token : undefined).then(() => false); }); } @@ -478,11 +479,11 @@ export class DebugService implements IDebugService { }); } - private launchOrAttachToSession(session: IDebugSession, focus = true): Promise { + private launchOrAttachToSession(session: IDebugSession, forceFocus = false): Promise { const dbgr = this.configurationManager.getDebugger(session.configuration.type); return session.initialize(dbgr!).then(() => { return session.launchOrAttach(session.configuration).then(() => { - if (focus) { + if (forceFocus || !this.viewModel.focusedSession) { this.focusStackFrame(undefined, undefined, session); } }); @@ -541,6 +542,10 @@ export class DebugService implements IDebugService { if (this.layoutService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue('debug').openExplorerOnEnd) { this.viewletService.openViewlet(EXPLORER_VIEWLET_ID); } + + // Data breakpoints that can not be persisted should be cleared when a session ends + const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.canPersist); + dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId())); } })); @@ -567,7 +572,7 @@ export class DebugService implements IDebugService { return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.extensionHostDebugService.reload(session.getId()) : undefined); } - const shouldFocus = this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId(); + const shouldFocus = !!this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId(); // If the restart is automatic -> disconnect, otherwise -> terminate #55064 return (isAutoRestart ? session.disconnect(true) : session.terminate(true)).then(() => { @@ -851,6 +856,8 @@ export class DebugService implements IDebugService { await this.sendBreakpoints(breakpoint.uri); } else if (breakpoint instanceof FunctionBreakpoint) { await this.sendFunctionBreakpoints(); + } else if (breakpoint instanceof DataBreakpoint) { + await this.sendDataBreakpoints(); } else { await this.sendExceptionBreakpoints(); } @@ -920,6 +927,19 @@ export class DebugService implements IDebugService { this.storeBreakpoints(); } + async addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + this.model.addDataBreakpoint(label, dataId, canPersist); + await this.sendDataBreakpoints(); + + this.storeBreakpoints(); + } + + async removeDataBreakpoints(id?: string): Promise { + this.model.removeDataBreakpoints(id); + await this.sendDataBreakpoints(); + this.storeBreakpoints(); + } + sendAllBreakpoints(session?: IDebugSession): Promise { return Promise.all(distinct(this.model.getBreakpoints(), bp => bp.uri.toString()).map(bp => this.sendBreakpoints(bp.uri, false, session))) .then(() => this.sendFunctionBreakpoints(session)) @@ -943,6 +963,14 @@ export class DebugService implements IDebugService { }); } + private sendDataBreakpoints(session?: IDebugSession): Promise { + const breakpointsToSend = this.model.getDataBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); + + return this.sendToOneOrAllSessions(session, s => { + return s.capabilities.supportsDataBreakpoints ? s.sendDataBreakpoints(breakpointsToSend) : Promise.resolve(undefined); + }); + } + private sendExceptionBreakpoints(session?: IDebugSession): Promise { const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); @@ -1006,6 +1034,17 @@ export class DebugService implements IDebugService { return result || []; } + private loadDataBreakpoints(): DataBreakpoint[] { + let result: DataBreakpoint[] | undefined; + try { + result = JSON.parse(this.storageService.get(DEBUG_DATA_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((dbp: any) => { + return new DataBreakpoint(dbp.label, dbp.dataId, true, dbp.enabled, dbp.hitCondition, dbp.condition, dbp.logMessage); + }); + } catch (e) { } + + return result || []; + } + private loadWatchExpressions(): Expression[] { let result: Expression[] | undefined; try { @@ -1041,6 +1080,13 @@ export class DebugService implements IDebugService { this.storageService.remove(DEBUG_FUNCTION_BREAKPOINTS_KEY, StorageScope.WORKSPACE); } + const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => dbp.canPersist); + if (dataBreakpoints.length) { + this.storageService.store(DEBUG_DATA_BREAKPOINTS_KEY, JSON.stringify(dataBreakpoints), StorageScope.WORKSPACE); + } else { + this.storageService.remove(DEBUG_DATA_BREAKPOINTS_KEY, StorageScope.WORKSPACE); + } + const exceptionBreakpoints = this.model.getExceptionBreakpoints(); if (exceptionBreakpoints.length) { this.storageService.store(DEBUG_EXCEPTION_BREAKPOINTS_KEY, JSON.stringify(exceptionBreakpoints), StorageScope.WORKSPACE); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index d454894401968..01151248238c7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { CompletionItem, completionKindFromString } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { mixin } from 'vs/base/common/objects'; import { Thread, ExpressionContainer, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -31,6 +31,8 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { onUnexpectedError } from 'vs/base/common/errors'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variablesView'; export class DebugSession implements IDebugSession { @@ -66,7 +68,8 @@ export class DebugSession implements IDebugSession { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @INotificationService private readonly notificationService: INotificationService, @IProductService private readonly productService: IProductService, - @IWindowsService private readonly windowsService: IWindowsService + @IWindowsService private readonly windowsService: IWindowsService, + @IOpenerService private readonly openerService: IOpenerService ) { this.id = generateUuid(); this.repl = new ReplModel(this); @@ -167,7 +170,7 @@ export class DebugSession implements IDebugSession { return dbgr.createDebugAdapter(this).then(debugAdapter => { - this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.windowsService); + this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.windowsService, this.openerService); return this.raw.start().then(() => { @@ -327,6 +330,34 @@ export class DebugSession implements IDebugSession { return Promise.reject(new Error('no debug adapter')); } + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }> { + if (this.raw) { + if (this.raw.readyForBreakpoints) { + return this.raw.dataBreakpointInfo({ name, variablesReference }).then(response => response.body); + } + return Promise.reject(new Error(nls.localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints"))); + } + return Promise.reject(new Error('no debug adapter')); + } + + sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise { + if (this.raw) { + if (this.raw.readyForBreakpoints) { + return this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints }).then(response => { + if (response && response.body) { + const data = new Map(); + for (let i = 0; i < dataBreakpoints.length; i++) { + data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); + } + this.model.setBreakpointSessionData(this.getId(), data); + } + }); + } + return Promise.resolve(undefined); + } + return Promise.reject(new Error('no debug adapter')); + } + customRequest(request: string, args: any): Promise { if (this.raw) { return this.raw.custom(request, args); @@ -524,7 +555,8 @@ export class DebugSession implements IDebugSession { insertText: item.text || item.label, kind: completionKindFromString(item.type || 'property'), filterText: (item.start && item.length) ? text.substr(item.start, item.length).concat(item.label) : undefined, - range: Range.fromPositions(position.delta(0, -(item.length || overwriteBefore)), position) + range: Range.fromPositions(position.delta(0, -(item.length || overwriteBefore)), position), + sortText: item.sortText }); } }); @@ -875,12 +907,13 @@ export class DebugSession implements IDebugSession { this._onDidChangeREPLElements.fire(); } - addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { + async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { const viewModel = this.debugService.getViewModel(); - return this.repl.addReplExpression(stackFrame, name) - .then(() => this._onDidChangeREPLElements.fire()) - // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. - .then(() => this.debugService.focusStackFrame(viewModel.focusedStackFrame, viewModel.focusedThread, viewModel.focusedSession)); + await this.repl.addReplExpression(stackFrame, name); + this._onDidChangeREPLElements.fire(); + // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. + this.debugService.focusStackFrame(viewModel.focusedStackFrame, viewModel.focusedThread, viewModel.focusedSession); + variableSetEmitter.fire(); } appendToRepl(data: string | IExpression, severity: severity, source?: IReplElementSource): void { diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 5146eac7f7810..ba384a37df3cc 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -131,6 +131,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); + this._register(this.debugService.onDidNewSession(() => this.updateScheduler.schedule())); this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { // check for error diff --git a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts index 3f5b703c4ce46..5be0fa24b5c60 100644 --- a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts @@ -17,7 +17,7 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient { constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, - //@IWindowService windowService: IWindowService, + // @IWindowService windowService: IWindowService, // TODO@weinand TODO@isidorn cyclic dependency? @IEnvironmentService environmentService: IEnvironmentService ) { const connection = remoteAgentService.getConnection(); @@ -30,13 +30,11 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient { this._register(this.onReload(event => { if (environmentService.isExtensionDevelopment && environmentService.debugExtensionHost.debugId === event.sessionId) { - //windowService.reloadWindow(); window.location.reload(); } })); this._register(this.onClose(event => { if (environmentService.isExtensionDevelopment && environmentService.debugExtensionHost.debugId === event.sessionId) { - //this._windowService.closeWindow(); window.close(); } })); diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 58cbb394e5f1e..df911b7d7c9f3 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import strings = require('vs/base/common/strings'); +import * as strings from 'vs/base/common/strings'; import { isAbsolute } from 'vs/base/common/path'; import { URI as uri } from 'vs/base/common/uri'; import { isMacintosh } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 39bb171c4ecc0..d8ad2212ace83 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -16,6 +16,8 @@ import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { env as processEnv } from 'vs/base/common/process'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; /** * This interface represents a single command line argument split into a "prefix" and a "path" half. @@ -73,7 +75,8 @@ export class RawDebugSession { dbgr: IDebugger, private readonly telemetryService: ITelemetryService, public readonly customTelemetryService: ITelemetryService | undefined, - private readonly windowsService: IWindowsService + private readonly windowsService: IWindowsService, + private readonly openerService: IOpenerService ) { this.debugAdapter = debugAdapter; @@ -356,6 +359,20 @@ export class RawDebugSession { return Promise.reject(new Error('setFunctionBreakpoints not supported')); } + dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise { + if (this.capabilities.supportsDataBreakpoints) { + return this.send('dataBreakpointInfo', args); + } + return Promise.reject(new Error('dataBreakpointInfo not supported')); + } + + setDataBreakpoints(args: DebugProtocol.SetDataBreakpointsArguments): Promise { + if (this.capabilities.supportsDataBreakpoints) { + return this.send('setDataBreakpoints', args); + } + return Promise.reject(new Error('setDataBreakpoints not supported')); + } + setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { return this.send('setExceptionBreakpoints', args); } @@ -594,10 +611,7 @@ export class RawDebugSession { let env: IProcessEnvironment = {}; if (vscodeArgs.env) { // merge environment variables into a copy of the process.env - if (typeof process === 'object' && process.env) { - env = objects.mixin(env, process.env); - } - env = objects.mixin(env, vscodeArgs.env); + env = objects.mixin(processEnv, vscodeArgs.env); // and delete some if necessary Object.keys(env).filter(k => env[k] === null).forEach(key => delete env[key]); } @@ -640,7 +654,7 @@ export class RawDebugSession { const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); return createErrorWithActions(userMessage, { actions: [new Action('debug.moreInfo', label, undefined, true, () => { - window.open(error.url); + this.openerService.open(URI.parse(error.url)); return Promise.resolve(null); })] }); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 938d2c6477c5d..96bb2f8da9da5 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -62,7 +62,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { VariablesRenderer, variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; const $ = dom.$; @@ -104,6 +104,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati private scopedInstantiationService!: IInstantiationService; private replElementsChangeListener: IDisposable | undefined; private styleElement: HTMLStyleElement | undefined; + private completionItemProvider: IDisposable | undefined; constructor( @IDebugService private readonly debugService: IDebugService, @@ -131,7 +132,33 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this._register(this.debugService.getViewModel().onDidFocusSession(session => { if (session) { sessionsToIgnore.delete(session); + if (this.completionItemProvider) { + this.completionItemProvider.dispose(); + } + if (session.capabilities.supportsCompletionsRequest) { + this.completionItemProvider = CompletionProviderRegistry.register({ scheme: DEBUG_SCHEME, pattern: '**/replinput', hasAccessToAllModels: true }, { + triggerCharacters: session.capabilities.completionTriggerCharacters || ['.'], + provideCompletionItems: async (_: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { + // Disable history navigation because up and down are used to navigate through the suggest widget + this.historyNavigationEnablement.set(false); + + const model = this.replInput.getModel(); + if (model) { + const word = model.getWordAtPosition(position); + const overwriteBefore = word ? word.word.length : 0; + const text = model.getLineContent(position.lineNumber); + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; + const suggestions = await session.completions(frameId, text, position, overwriteBefore); + return { suggestions }; + } + + return Promise.resolve({ suggestions: [] }); + } + }); + } } + this.selectSession(); })); this._register(this.debugService.onWillNewSession(newSession => { @@ -274,7 +301,6 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati revealLastElement(this.tree); this.history.add(this.replInput.getValue()); this.replInput.setValue(''); - variableSetEmitter.fire(); const shouldRelayout = this.replInputHeight > Repl.REPL_INPUT_INITIAL_HEIGHT; this.replInputHeight = Repl.REPL_INPUT_INITIAL_HEIGHT; if (shouldRelayout) { @@ -418,6 +444,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati private createReplInput(container: HTMLElement): void { this.replInputContainer = dom.append(container, $('.repl-input-wrapper')); + this.replInputContainer.title = nls.localize('debugConsole', "Debug Console"); const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: this.replInputContainer, historyNavigator: this }); this.historyNavigationEnablement = historyNavigationEnablement; @@ -430,34 +457,6 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati options.readOnly = true; this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions()); - CompletionProviderRegistry.register({ scheme: DEBUG_SCHEME, pattern: '**/replinput', hasAccessToAllModels: true }, { - triggerCharacters: ['.'], - provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { - // Disable history navigation because up and down are used to navigate through the suggest widget - this.historyNavigationEnablement.set(false); - - const focusedSession = this.debugService.getViewModel().focusedSession; - if (focusedSession && focusedSession.capabilities.supportsCompletionsRequest) { - - const model = this.replInput.getModel(); - if (model) { - const word = model.getWordAtPosition(position); - const overwriteBefore = word ? word.word.length : 0; - const text = model.getLineContent(position.lineNumber); - const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; - - return focusedSession.completions(frameId, text, position, overwriteBefore).then(suggestions => { - return { suggestions }; - }, err => { - return { suggestions: [] }; - }); - } - } - return Promise.resolve({ suggestions: [] }); - } - }); - this._register(this.replInput.onDidScrollChange(e => { if (!e.scrollHeightChanged) { return; diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 45eb8037c717b..052f2506db109 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -123,7 +123,7 @@ export class VariablesView extends ViewletPanel { this.tree.updateChildren(); })); this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); + this._register(this.tree.onContextMenu(async e => await this.onContextMenu(e))); this._register(this.onDidChangeBodyVisibility(visible => { if (visible && this.needsRefresh) { @@ -152,7 +152,7 @@ export class VariablesView extends ViewletPanel { } } - private onContextMenu(e: ITreeContextMenuEvent): void { + private async onContextMenu(e: ITreeContextMenuEvent): Promise { const variable = e.element; if (variable instanceof Variable && !!variable.value) { const actions: IAction[] = []; @@ -174,6 +174,16 @@ export class VariablesView extends ViewletPanel { return Promise.resolve(undefined); })); } + if (session && session.capabilities.supportsDataBreakpoints) { + const response = await session.dataBreakpointInfo(variable.name, variable.parent.reference); + const dataid = response.dataId; + if (dataid) { + actions.push(new Separator()); + actions.push(new Action('debug.addDataBreakpoint', nls.localize('setDataBreakpoint', "Set Data Breakpoint"), undefined, true, () => { + return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist); + })); + } + } this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 65e28660dc9aa..42a3576979723 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -103,6 +103,7 @@ export interface IReplElementSource { export interface IExpressionContainer extends ITreeElement { readonly hasChildren: boolean; getChildren(): Promise; + readonly reference?: number; } export interface IExpression extends IReplElement, IExpressionContainer { @@ -201,6 +202,8 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }>; + sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; stackTrace(threadId: number, startFrame: number, levels: number): Promise; @@ -357,6 +360,12 @@ export interface IExceptionBreakpoint extends IEnablement { readonly label: string; } +export interface IDataBreakpoint extends IBaseBreakpoint { + readonly label: string; + readonly dataId: string; + readonly canPersist: boolean; +} + export interface IExceptionInfo { readonly id?: string; readonly description?: string; @@ -404,6 +413,7 @@ export interface IDebugModel extends ITreeElement { getBreakpoints(filter?: { uri?: uri, lineNumber?: number, column?: number, enabledOnly?: boolean }): ReadonlyArray; areBreakpointsActivated(): boolean; getFunctionBreakpoints(): ReadonlyArray; + getDataBreakpoints(): ReadonlyArray; getExceptionBreakpoints(): ReadonlyArray; getWatchExpressions(): ReadonlyArray; @@ -416,9 +426,9 @@ export interface IDebugModel extends ITreeElement { * An event describing a change to the set of [breakpoints](#debug.Breakpoint). */ export interface IBreakpointsChangeEvent { - added?: Array; - removed?: Array; - changed?: Array; + added?: Array; + removed?: Array; + changed?: Array; sessionOnly?: boolean; } @@ -754,6 +764,17 @@ export interface IDebugService { */ removeFunctionBreakpoints(id?: string): Promise; + /** + * Adds a new data breakpoint. + */ + addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise; + + /** + * Removes all data breakpoints. If id is passed only removes the data breakpoint with the passed id. + * Notifies debug adapter of breakpoint changes. + */ + removeDataBreakpoints(id?: string): Promise; + /** * Sends all breakpoints to the passed session. * If session is not passed, sends all breakpoints to each session. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 7166ad996a718..599c496dcd97a 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -16,7 +16,7 @@ import { distinct, lastIndex } from 'vs/base/common/arrays'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, IReplElementSource, - IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State + IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL } from 'vs/workbench/contrib/debug/common/debugSource'; import { commonSuffixLength } from 'vs/base/common/strings'; @@ -227,14 +227,14 @@ export class Expression extends ExpressionContainer implements IExpression { const response = await session.evaluate(this.name, stackFrame ? stackFrame.frameId : undefined, context); this.available = !!(response && response.body); if (response && response.body) { - this.value = response.body.result; + this.value = response.body.result || ''; this.reference = response.body.variablesReference; this.namedVariables = response.body.namedVariables; this.indexedVariables = response.body.indexedVariables; this.type = response.body.type || this.type; } } catch (e) { - this.value = e.message; + this.value = e.message || ''; this.available = false; this.reference = 0; } @@ -256,7 +256,7 @@ export class Variable extends ExpressionContainer implements IExpression { reference: number | undefined, public name: string, public evaluateName: string | undefined, - value: string, + value: string | undefined, namedVariables: number | undefined, indexedVariables: number | undefined, public presentationHint: DebugProtocol.VariablePresentationHint | undefined, @@ -265,7 +265,7 @@ export class Variable extends ExpressionContainer implements IExpression { startOfVariables = 0 ) { super(session, reference, `variable:${parent.getId()}:${name}`, namedVariables, indexedVariables, startOfVariables); - this.value = value; + this.value = value || ''; } async setVariable(value: string): Promise { @@ -276,7 +276,7 @@ export class Variable extends ExpressionContainer implements IExpression { try { const response = await this.session.setVariable((this.parent).reference, this.name, value); if (response && response.body) { - this.value = response.body.value; + this.value = response.body.value || ''; this.type = response.body.type || this.type; this.reference = response.body.variablesReference; this.namedVariables = response.body.namedVariables; @@ -735,6 +735,34 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak } } +export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { + + constructor( + public label: string, + public dataId: string, + public canPersist: boolean, + enabled: boolean, + hitCondition: string | undefined, + condition: string | undefined, + logMessage: string | undefined, + id = generateUuid() + ) { + super(enabled, hitCondition, condition, logMessage, id); + } + + toJSON(): any { + const result = super.toJSON(); + result.label = this.label; + result.dataid = this.dataId; + + return result; + } + + toString(): string { + return this.label; + } +} + export class ExceptionBreakpoint extends Enablement implements IExceptionBreakpoint { constructor(public filter: string, public label: string, enabled: boolean) { @@ -778,6 +806,7 @@ export class DebugModel implements IDebugModel { private breakpointsActivated: boolean, private functionBreakpoints: FunctionBreakpoint[], private exceptionBreakpoints: ExceptionBreakpoint[], + private dataBreakopints: DataBreakpoint[], private watchExpressions: Expression[], private textFileService: ITextFileService ) { @@ -918,6 +947,10 @@ export class DebugModel implements IDebugModel { return this.functionBreakpoints; } + getDataBreakpoints(): IDataBreakpoint[] { + return this.dataBreakopints; + } + getExceptionBreakpoints(): IExceptionBreakpoint[] { return this.exceptionBreakpoints; } @@ -991,6 +1024,12 @@ export class DebugModel implements IDebugModel { fbp.setSessionData(sessionId, fbpData); } }); + this.dataBreakopints.forEach(dbp => { + const dbpData = data.get(dbp.getId()); + if (dbpData) { + dbp.setSessionData(sessionId, dbpData); + } + }); this._onDidChangeBreakpoints.fire({ sessionOnly: true @@ -1001,6 +1040,7 @@ export class DebugModel implements IDebugModel { this.breakpointsSessionId = sessionId; this.breakpoints.forEach(bp => bp.setSessionId(sessionId)); this.functionBreakpoints.forEach(fbp => fbp.setSessionId(sessionId)); + this.dataBreakopints.forEach(dbp => dbp.setSessionId(sessionId)); this._onDidChangeBreakpoints.fire({ sessionOnly: true @@ -1038,7 +1078,7 @@ export class DebugModel implements IDebugModel { } enableOrDisableAllBreakpoints(enable: boolean): void { - const changed: Array = []; + const changed: Array = []; this.breakpoints.forEach(bp => { if (bp.enabled !== enable) { @@ -1052,6 +1092,12 @@ export class DebugModel implements IDebugModel { } fbp.enabled = enable; }); + this.dataBreakopints.forEach(dbp => { + if (dbp.enabled !== enable) { + changed.push(dbp); + } + dbp.enabled = enable; + }); this._onDidChangeBreakpoints.fire({ changed: changed }); } @@ -1073,7 +1119,6 @@ export class DebugModel implements IDebugModel { } removeFunctionBreakpoints(id?: string): void { - let removed: FunctionBreakpoint[]; if (id) { removed = this.functionBreakpoints.filter(fbp => fbp.getId() === id); @@ -1082,7 +1127,25 @@ export class DebugModel implements IDebugModel { removed = this.functionBreakpoints; this.functionBreakpoints = []; } - this._onDidChangeBreakpoints.fire({ removed: removed }); + this._onDidChangeBreakpoints.fire({ removed }); + } + + addDataBreakpoint(label: string, dataId: string, canPersist: boolean): void { + const newDataBreakpoint = new DataBreakpoint(label, dataId, canPersist, true, undefined, undefined, undefined); + this.dataBreakopints.push(newDataBreakpoint); + this._onDidChangeBreakpoints.fire({ added: [newDataBreakpoint] }); + } + + removeDataBreakpoints(id?: string): void { + let removed: DataBreakpoint[]; + if (id) { + removed = this.dataBreakopints.filter(fbp => fbp.getId() === id); + this.dataBreakopints = this.dataBreakopints.filter(fbp => fbp.getId() !== id); + } else { + removed = this.dataBreakopints; + this.dataBreakopints = []; + } + this._onDidChangeBreakpoints.fire({ removed }); } getWatchExpressions(): Expression[] { diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 39fbff2964f99..8854146a48484 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -168,7 +168,7 @@ declare module DebugProtocol { category?: string; /** The output to report. */ output: string; - /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. */ + /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. The value should be less than or equal to 2147483647 (2^31 - 1). */ variablesReference?: number; /** An optional source location where the output was produced. */ source?: Source; @@ -284,9 +284,9 @@ declare module DebugProtocol { /** Response to 'runInTerminal' request. */ export interface RunInTerminalResponse extends Response { body: { - /** The process ID. */ + /** The process ID. The value should be less than or equal to 2147483647 (2^31 - 1). */ processId?: number; - /** The process ID of the terminal shell. */ + /** The process ID of the terminal shell. The value should be less than or equal to 2147483647 (2^31 - 1). */ shellProcessId?: number; }; } @@ -757,7 +757,7 @@ declare module DebugProtocol { } /** Pause request; value of command field is 'pause'. - The request suspenses the debuggee. + The request suspends the debuggee. The debug adapter first sends the response and then a 'stopped' event (with reason 'pause') after the thread has been paused successfully. */ export interface PauseRequest extends Request { @@ -842,7 +842,7 @@ declare module DebugProtocol { export interface VariablesArguments { /** The Variable reference. */ variablesReference: number; - /** Optional filter to limit the child variables to either named or indexed. If ommited, both types are fetched. */ + /** Optional filter to limit the child variables to either named or indexed. If omitted, both types are fetched. */ filter?: 'indexed' | 'named'; /** The index of the first variable to return; if omitted children start at 0. */ start?: number; @@ -887,14 +887,14 @@ declare module DebugProtocol { value: string; /** The type of the new value. Typically shown in the UI when hovering over the value. */ type?: string; - /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ + /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ variablesReference?: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; }; @@ -1041,14 +1041,14 @@ declare module DebugProtocol { type?: string; /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ + /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ variablesReference: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; /** Memory reference to a location appropriate for this result. For pointer type eval results, this is generally a reference to the memory address contained in the pointer. */ @@ -1086,14 +1086,14 @@ declare module DebugProtocol { type?: string; /** Properties of a value that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ + /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ variablesReference?: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; }; @@ -1294,6 +1294,8 @@ declare module DebugProtocol { supportsStepInTargetsRequest?: boolean; /** The debug adapter supports the 'completions' request. */ supportsCompletionsRequest?: boolean; + /** The set of characters that should trigger completion in a REPL. If not specified, the UI should assume the '.' character. */ + completionTriggerCharacters?: string[]; /** The debug adapter supports the 'modules' request. */ supportsModulesRequest?: boolean; /** The set of additional module information exposed by the debug adapter. */ @@ -1433,7 +1435,7 @@ declare module DebugProtocol { name?: string; /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). */ path?: string; - /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. */ + /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. The value should be less than or equal to 2147483647 (2^31 - 1). */ sourceReference?: number; /** An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. */ presentationHint?: 'normal' | 'emphasize' | 'deemphasize'; @@ -1668,6 +1670,8 @@ declare module DebugProtocol { label: string; /** If text is not falsy then it is inserted instead of the label. */ text?: string; + /** A string that should be used when comparing this item with other items. When `falsy` the label is used. */ + sortText?: string; /** The item's type. Typically the client uses this information to render the item in the UI with an icon. */ type?: CompletionItemType; /** This value determines the location (in the CompletionsRequest's 'text' attribute) where the completion text is added. @@ -1729,7 +1733,7 @@ declare module DebugProtocol { /** This enumeration defines all possible conditions when a thrown exception should result in a break. never: never breaks, always: always breaks, - unhandled: breaks when excpetion unhandled, + unhandled: breaks when exception unhandled, userUnhandled: breaks if the exception is not handled by user code. */ export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; @@ -1766,7 +1770,7 @@ declare module DebugProtocol { instructionBytes?: string; /** Text representing the instruction and its operands, in an implementation-defined format. */ instruction: string; - /** Name of the symbol that correponds with the location of this instruction, if any. */ + /** Name of the symbol that corresponds with the location of this instruction, if any. */ symbol?: string; /** Source location that corresponds to this instruction, if any. Should always be set (if available) on the first instruction returned, but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. */ location?: Source; diff --git a/src/vs/workbench/contrib/debug/common/debugSource.ts b/src/vs/workbench/contrib/debug/common/debugSource.ts index 133e3f8bec8d9..fa0376d936e69 100644 --- a/src/vs/workbench/contrib/debug/common/debugSource.ts +++ b/src/vs/workbench/contrib/debug/common/debugSource.ts @@ -13,6 +13,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/ import { Schemas } from 'vs/base/common/network'; import { isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ITextEditor } from 'vs/workbench/common/editor'; +import { withUndefinedAsNull } from 'vs/base/common/types'; export const UNKNOWN_SOURCE_LABEL = nls.localize('unknownSource', "Unknown Source"); @@ -104,7 +105,7 @@ export class Source { revealInCenterIfOutsideViewport: true, pinned: pinned || (!preserveFocus && !this.inMemory) } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(withUndefinedAsNull); } static getEncodedDebugData(modelUri: uri): { name: string, path: string, sessionId?: string, sourceReference?: number } { diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 8f317ccc138f7..8559889910ed0 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -236,16 +236,18 @@ export class Debugger implements IDebugger { }; properties['preLaunchTask'] = { anyOf: [taskSchema, { - type: ['string', 'null'], + type: ['string'] }], default: '', + defaultSnippets: [{ body: { task: '', type: '' } }], description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") }; properties['postDebugTask'] = { anyOf: [taskSchema, { - type: ['string', 'null'], + type: ['string'], }], default: '', + defaultSnippets: [{ body: { task: '', type: '' } }], description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") }; properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts index 6b313596ca61c..d967dce1868f0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts @@ -13,9 +13,10 @@ import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { NullOpenerService } from 'vs/platform/opener/common/opener'; function createMockSession(model: DebugModel, name = 'mockSession', parentSession?: DebugSession | undefined): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); } suite('Debug - Model', () => { @@ -23,7 +24,7 @@ suite('Debug - Model', () => { let rawSession: MockRawSession; setup(() => { - model = new DebugModel([], true, [], [], [], { isDirty: (e: any) => false }); + model = new DebugModel([], true, [], [], [], [], { isDirty: (e: any) => false }); rawSession = new MockRawSession(); }); @@ -427,7 +428,7 @@ suite('Debug - Model', () => { // Repl output test('repl output', () => { - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); const repl = new ReplModel(session); repl.appendToRepl('first line\n', severity.Error); repl.appendToRepl('second line ', severity.Error); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 19aae90891d6c..bb10110ab92cc 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -7,7 +7,7 @@ import { URI as uri } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Position } from 'vs/editor/common/core/position'; -import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource } from 'vs/workbench/contrib/debug/common/debug'; +import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { CompletionItem } from 'vs/editor/common/modes'; import Severity from 'vs/base/common/severity'; @@ -79,6 +79,13 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } + addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + throw new Error('Method not implemented.'); + } + removeDataBreakpoints(id?: string | undefined): Promise { + throw new Error('Method not implemented.'); + } + public addReplExpression(name: string): Promise { throw new Error('not implemented'); } @@ -125,6 +132,13 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined; }> { + throw new Error('Method not implemented.'); + } + + sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise { + throw new Error('Method not implemented.'); + } subId: string | undefined; diff --git a/src/vs/workbench/contrib/experiments/electron-browser/experimentalPrompt.ts b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts similarity index 94% rename from src/vs/workbench/contrib/experiments/electron-browser/experimentalPrompt.ts rename to src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts index 97f55700a404a..b8ded4b910a56 100644 --- a/src/vs/workbench/contrib/experiments/electron-browser/experimentalPrompt.ts +++ b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts @@ -11,6 +11,8 @@ import { IExtensionsViewlet } from 'vs/workbench/contrib/extensions/common/exten import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable } from 'vs/base/common/lifecycle'; import { language } from 'vs/base/common/platform'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export class ExperimentalPrompts extends Disposable implements IWorkbenchContribution { @@ -18,7 +20,8 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib @IExperimentService private readonly experimentService: IExperimentService, @IViewletService private readonly viewletService: IViewletService, @INotificationService private readonly notificationService: INotificationService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); @@ -65,7 +68,7 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib run: () => { logTelemetry(commandText); if (command.externalLink) { - window.open(command.externalLink); + this.openerService.open(URI.parse(command.externalLink)); } else if (command.curatedExtensionsKey && Array.isArray(command.curatedExtensionsList)) { this.viewletService.openViewlet('workbench.view.extensions', true) .then(viewlet => viewlet as IExtensionsViewlet) diff --git a/src/vs/workbench/contrib/experiments/electron-browser/experiments.contribution.ts b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts similarity index 79% rename from src/vs/workbench/contrib/experiments/electron-browser/experiments.contribution.ts rename to src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts index ca28e9b6f67d3..67b6159734f82 100644 --- a/src/vs/workbench/contrib/experiments/electron-browser/experiments.contribution.ts +++ b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { ExperimentService } from 'vs/workbench/contrib/experiments/electron-browser/experimentService'; +import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/electron-browser/experimentalPrompt'; +import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; registerSingleton(IExperimentService, ExperimentService, true); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 437afe18bd6e5..5707377ce4b1b 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -4,7 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { language } from 'vs/base/common/platform'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { match } from 'vs/base/common/glob'; +import { IRequestService, asJson } from 'vs/platform/request/common/request'; +import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { distinct } from 'vs/base/common/arrays'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IProductService } from 'vs/platform/product/common/product'; +import { IWorkspaceStatsService } from 'vs/workbench/contrib/stats/common/workspaceStats'; export const enum ExperimentState { Evaluating, @@ -56,4 +72,388 @@ export interface IExperimentService { onExperimentEnabled: Event; } -export const IExperimentService = createDecorator('experimentService'); \ No newline at end of file +export const IExperimentService = createDecorator('experimentService'); + +interface IExperimentStorageState { + enabled: boolean; + state: ExperimentState; + editCount?: number; + lastEditedDate?: string; +} + +interface IRawExperiment { + id: string; + enabled?: boolean; + condition?: { + insidersOnly?: boolean; + newUser?: boolean; + displayLanguage?: string; + installedExtensions?: { + excludes?: string[]; + includes?: string[]; + }, + fileEdits?: { + filePathPattern?: string; + workspaceIncludes?: string[]; + workspaceExcludes?: string[]; + minEditCount: number; + }, + experimentsPreviouslyRun?: { + excludes?: string[]; + includes?: string[]; + } + userProbability?: number; + }; + action?: IExperimentAction; +} + +export class ExperimentService extends Disposable implements IExperimentService { + _serviceBrand: any; + private _experiments: IExperiment[] = []; + private _loadExperimentsPromise: Promise; + private _curatedMapping = Object.create(null); + + private readonly _onExperimentEnabled = this._register(new Emitter()); + onExperimentEnabled: Event = this._onExperimentEnabled.event; + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @ITextFileService private readonly textFileService: ITextFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IRequestService private readonly requestService: IRequestService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, + @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService + ) { + super(); + + this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments()); + } + + public getExperimentById(id: string): Promise { + return this._loadExperimentsPromise.then(() => { + return this._experiments.filter(x => x.id === id)[0]; + }); + } + + public getExperimentsByType(type: ExperimentActionType): Promise { + return this._loadExperimentsPromise.then(() => { + if (type === ExperimentActionType.Custom) { + return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type)); + } + return this._experiments.filter(x => x.enabled && x.action && x.action.type === type); + }); + } + + public getCuratedExtensionsList(curatedExtensionsKey: string): Promise { + return this._loadExperimentsPromise.then(() => { + for (const experiment of this._experiments) { + if (experiment.enabled + && experiment.state === ExperimentState.Run + && this._curatedMapping[experiment.id] + && this._curatedMapping[experiment.id].curatedExtensionsKey === curatedExtensionsKey) { + return this._curatedMapping[experiment.id].curatedExtensionsList; + } + } + return []; + }); + } + + public markAsCompleted(experimentId: string): void { + const storageKey = 'experiments.' + experimentId; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + experimentState.state = ExperimentState.Complete; + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); + } + + protected getExperiments(): Promise { + if (!this.productService.experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { + return Promise.resolve([]); + } + return this.requestService.request({ type: 'GET', url: this.productService.experimentsUrl }, CancellationToken.None).then(context => { + if (context.res.statusCode !== 200) { + return Promise.resolve(null); + } + return asJson(context).then((result: any) => { + return result && Array.isArray(result['experiments']) ? result['experiments'] : []; + }); + }, () => Promise.resolve(null)); + } + + private loadExperiments(): Promise { + return this.getExperiments().then(rawExperiments => { + // Offline mode + if (!rawExperiments) { + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + if (Array.isArray(allExperimentIdsFromStorage)) { + allExperimentIdsFromStorage.forEach(experimentId => { + const storageKey = 'experiments.' + experimentId; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null); + if (experimentState) { + this._experiments.push({ + id: experimentId, + enabled: experimentState.enabled, + state: experimentState.state + }); + } + }); + } + return Promise.resolve(null); + } + + // Clear disbaled/deleted experiments from storage + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); + if (Array.isArray(allExperimentIdsFromStorage)) { + allExperimentIdsFromStorage.forEach(experiment => { + if (enabledExperiments.indexOf(experiment) === -1) { + this.storageService.remove(`experiments.${experiment}`, StorageScope.GLOBAL); + } + }); + } + if (enabledExperiments.length) { + this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL); + } else { + this.storageService.remove('allExperiments', StorageScope.GLOBAL); + } + + const promises = rawExperiments.map(experiment => { + const processedExperiment: IExperiment = { + id: experiment.id, + enabled: !!experiment.enabled, + state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun + }; + + if (experiment.action) { + processedExperiment.action = { + type: ExperimentActionType[experiment.action.type] || ExperimentActionType.Custom, + properties: experiment.action.properties + }; + if (processedExperiment.action.type === ExperimentActionType.Prompt) { + ((processedExperiment.action.properties).commands || []).forEach(x => { + if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) { + this._curatedMapping[experiment.id] = x; + } + }); + } + if (!processedExperiment.action.properties) { + processedExperiment.action.properties = {}; + } + } + this._experiments.push(processedExperiment); + + if (!processedExperiment.enabled) { + return Promise.resolve(null); + } + + const storageKey = 'experiments.' + experiment.id; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + if (!experimentState.hasOwnProperty('enabled')) { + experimentState.enabled = processedExperiment.enabled; + } + if (!experimentState.hasOwnProperty('state')) { + experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun; + } else { + processedExperiment.state = experimentState.state; + } + + return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => { + experimentState.state = processedExperiment.state = state; + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); + + if (state === ExperimentState.Run) { + this.fireRunExperiment(processedExperiment); + } + return Promise.resolve(null); + }); + + }); + return Promise.all(promises).then(() => { + type ExperimentsClassification = { + experiments: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + this.telemetryService.publicLog2<{ experiments: IExperiment[] }, ExperimentsClassification>('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) === -1) { + runExperimentIdsFromStorage.push(experiment.id); + } + + // Ensure we dont store duplicates + const distinctExperiments = distinct(runExperimentIdsFromStorage); + if (runExperimentIdsFromStorage.length !== distinctExperiments.length) { + this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.GLOBAL); + } + } + + private checkExperimentDependencies(experiment: IRawExperiment): boolean { + const experimentsPreviouslyRun = experiment.condition ? experiment.condition.experimentsPreviouslyRun : undefined; + if (experimentsPreviouslyRun) { + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + let includeCheck = true; + let excludeCheck = true; + const includes = experimentsPreviouslyRun.includes; + if (Array.isArray(includes)) { + includeCheck = runExperimentIdsFromStorage.some(x => includes.indexOf(x) > -1); + } + const excludes = experimentsPreviouslyRun.excludes; + if (includeCheck && Array.isArray(excludes)) { + excludeCheck = !runExperimentIdsFromStorage.some(x => excludes.indexOf(x) > -1); + } + if (!includeCheck || !excludeCheck) { + return false; + } + } + return true; + } + + private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise { + if (processedExperiment.state !== ExperimentState.Evaluating) { + return Promise.resolve(processedExperiment.state); + } + + if (!experiment.enabled) { + return Promise.resolve(ExperimentState.NoRun); + } + + const condition = experiment.condition; + if (!condition) { + return Promise.resolve(ExperimentState.Run); + } + + if (!this.checkExperimentDependencies(experiment)) { + return Promise.resolve(ExperimentState.NoRun); + } + + if (this.environmentService.appQuality === 'stable' && condition.insidersOnly === true) { + return Promise.resolve(ExperimentState.NoRun); + } + + const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL); + if ((condition.newUser === true && !isNewUser) + || (condition.newUser === false && isNewUser)) { + return Promise.resolve(ExperimentState.NoRun); + } + + if (typeof condition.displayLanguage === 'string') { + let localeToCheck = condition.displayLanguage.toLowerCase(); + let displayLanguage = language!.toLowerCase(); + + if (localeToCheck !== displayLanguage) { + const a = displayLanguage.indexOf('-'); + const b = localeToCheck.indexOf('-'); + if (a > -1) { + displayLanguage = displayLanguage.substr(0, a); + } + if (b > -1) { + localeToCheck = localeToCheck.substr(0, b); + } + if (displayLanguage !== localeToCheck) { + return Promise.resolve(ExperimentState.NoRun); + } + } + } + + if (!condition.userProbability) { + condition.userProbability = 1; + } + + let extensionsCheckPromise = Promise.resolve(true); + const installedExtensions = condition.installedExtensions; + if (installedExtensions) { + extensionsCheckPromise = this.extensionManagementService.getInstalled(ExtensionType.User).then(locals => { + let includesCheck = true; + let excludesCheck = true; + const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`); + if (Array.isArray(installedExtensions.includes) && installedExtensions.includes.length) { + const extensionIncludes = installedExtensions.includes.map(e => e.toLowerCase()); + includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1); + } + if (Array.isArray(installedExtensions.excludes) && installedExtensions.excludes.length) { + const extensionExcludes = installedExtensions.excludes.map(e => e.toLowerCase()); + excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1); + } + return includesCheck && excludesCheck; + }); + } + + const storageKey = 'experiments.' + experiment.id; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + + return extensionsCheckPromise.then(success => { + const fileEdits = condition.fileEdits; + if (!success || !fileEdits || typeof fileEdits.minEditCount !== 'number') { + const runExperiment = success && typeof condition.userProbability === 'number' && Math.random() < condition.userProbability; + return runExperiment ? ExperimentState.Run : ExperimentState.NoRun; + } + + experimentState.editCount = experimentState.editCount || 0; + if (experimentState.editCount >= fileEdits.minEditCount) { + return ExperimentState.Run; + } + + const onSaveHandler = this.textFileService.models.onModelsSaved(e => { + const date = new Date().toDateString(); + const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + if (latestExperimentState.state !== ExperimentState.Evaluating) { + onSaveHandler.dispose(); + return; + } + e.forEach(async event => { + if (event.kind !== StateChange.SAVED + || latestExperimentState.state !== ExperimentState.Evaluating + || date === latestExperimentState.lastEditedDate + || (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) + ) { + return; + } + let filePathCheck = true; + let workspaceCheck = true; + + if (typeof fileEdits.filePathPattern === 'string') { + filePathCheck = match(fileEdits.filePathPattern, event.resource.fsPath); + } + if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) { + const tags = await this.workspaceStatsService.getTags(); + workspaceCheck = !!tags && fileEdits.workspaceIncludes.some(x => !!tags[x]); + } + if (workspaceCheck && Array.isArray(fileEdits.workspaceExcludes) && fileEdits.workspaceExcludes.length) { + const tags = await this.workspaceStatsService.getTags(); + workspaceCheck = !!tags && !fileEdits.workspaceExcludes.some(x => !!tags[x]); + } + if (filePathCheck && workspaceCheck) { + latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1; + latestExperimentState.lastEditedDate = date; + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); + } + }); + if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) { + processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun; + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); + if (latestExperimentState.state === ExperimentState.Run && experiment.action && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) { + this.fireRunExperiment(processedExperiment); + } + } + }); + this._register(onSaveHandler); + return ExperimentState.Evaluating; + }); + } +} + + +function safeParse(text: string | undefined, defaultObject: any) { + try { + return text ? JSON.parse(text) || defaultObject : defaultObject; + } catch (e) { + return defaultObject; + } +} diff --git a/src/vs/workbench/contrib/experiments/electron-browser/experimentService.ts b/src/vs/workbench/contrib/experiments/electron-browser/experimentService.ts deleted file mode 100644 index 79dd609cb6076..0000000000000 --- a/src/vs/workbench/contrib/experiments/electron-browser/experimentService.ts +++ /dev/null @@ -1,407 +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 { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { language } from 'vs/base/common/platform'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { match } from 'vs/base/common/glob'; -import { IRequestService, asJson } from 'vs/platform/request/common/request'; -import { Emitter, Event } from 'vs/base/common/event'; -import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { distinct } from 'vs/base/common/arrays'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { ExperimentState, IExperimentAction, IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { IProductService } from 'vs/platform/product/common/product'; -import { IWorkspaceStatsService } from 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; - -interface IExperimentStorageState { - enabled: boolean; - state: ExperimentState; - editCount?: number; - lastEditedDate?: string; -} - -interface IRawExperiment { - id: string; - enabled?: boolean; - condition?: { - insidersOnly?: boolean; - newUser?: boolean; - displayLanguage?: string; - installedExtensions?: { - excludes?: string[]; - includes?: string[]; - }, - fileEdits?: { - filePathPattern?: string; - workspaceIncludes?: string[]; - workspaceExcludes?: string[]; - minEditCount: number; - }, - experimentsPreviouslyRun?: { - excludes?: string[]; - includes?: string[]; - } - userProbability?: number; - }; - action?: IExperimentAction; -} - -export class ExperimentService extends Disposable implements IExperimentService { - _serviceBrand: any; - private _experiments: IExperiment[] = []; - private _loadExperimentsPromise: Promise; - private _curatedMapping = Object.create(null); - - private readonly _onExperimentEnabled = this._register(new Emitter()); - onExperimentEnabled: Event = this._onExperimentEnabled.event; - - constructor( - @IStorageService private readonly storageService: IStorageService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @ITextFileService private readonly textFileService: ITextFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IRequestService private readonly requestService: IRequestService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IProductService private readonly productService: IProductService, - @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService - ) { - super(); - - this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments()); - } - - public getExperimentById(id: string): Promise { - return this._loadExperimentsPromise.then(() => { - return this._experiments.filter(x => x.id === id)[0]; - }); - } - - public getExperimentsByType(type: ExperimentActionType): Promise { - return this._loadExperimentsPromise.then(() => { - if (type === ExperimentActionType.Custom) { - return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type)); - } - return this._experiments.filter(x => x.enabled && x.action && x.action.type === type); - }); - } - - public getCuratedExtensionsList(curatedExtensionsKey: string): Promise { - return this._loadExperimentsPromise.then(() => { - for (const experiment of this._experiments) { - if (experiment.enabled - && experiment.state === ExperimentState.Run - && this._curatedMapping[experiment.id] - && this._curatedMapping[experiment.id].curatedExtensionsKey === curatedExtensionsKey) { - return this._curatedMapping[experiment.id].curatedExtensionsList; - } - } - return []; - }); - } - - public markAsCompleted(experimentId: string): void { - const storageKey = 'experiments.' + experimentId; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - experimentState.state = ExperimentState.Complete; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); - } - - protected getExperiments(): Promise { - if (!this.productService.experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { - return Promise.resolve([]); - } - return this.requestService.request({ type: 'GET', url: this.productService.experimentsUrl }, CancellationToken.None).then(context => { - if (context.res.statusCode !== 200) { - return Promise.resolve(null); - } - return asJson(context).then((result: any) => { - return result && Array.isArray(result['experiments']) ? result['experiments'] : []; - }); - }, () => Promise.resolve(null)); - } - - private loadExperiments(): Promise { - return this.getExperiments().then(rawExperiments => { - // Offline mode - if (!rawExperiments) { - const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); - if (Array.isArray(allExperimentIdsFromStorage)) { - allExperimentIdsFromStorage.forEach(experimentId => { - const storageKey = 'experiments.' + experimentId; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null); - if (experimentState) { - this._experiments.push({ - id: experimentId, - enabled: experimentState.enabled, - state: experimentState.state - }); - } - }); - } - return Promise.resolve(null); - } - - // Clear disbaled/deleted experiments from storage - const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); - const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); - if (Array.isArray(allExperimentIdsFromStorage)) { - allExperimentIdsFromStorage.forEach(experiment => { - if (enabledExperiments.indexOf(experiment) === -1) { - this.storageService.remove(`experiments.${experiment}`, StorageScope.GLOBAL); - } - }); - } - if (enabledExperiments.length) { - this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL); - } else { - this.storageService.remove('allExperiments', StorageScope.GLOBAL); - } - - const promises = rawExperiments.map(experiment => { - const processedExperiment: IExperiment = { - id: experiment.id, - enabled: !!experiment.enabled, - state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun - }; - - if (experiment.action) { - processedExperiment.action = { - type: ExperimentActionType[experiment.action.type] || ExperimentActionType.Custom, - properties: experiment.action.properties - }; - if (processedExperiment.action.type === ExperimentActionType.Prompt) { - ((processedExperiment.action.properties).commands || []).forEach(x => { - if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) { - this._curatedMapping[experiment.id] = x; - } - }); - } - if (!processedExperiment.action.properties) { - processedExperiment.action.properties = {}; - } - } - this._experiments.push(processedExperiment); - - if (!processedExperiment.enabled) { - return Promise.resolve(null); - } - - const storageKey = 'experiments.' + experiment.id; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - if (!experimentState.hasOwnProperty('enabled')) { - experimentState.enabled = processedExperiment.enabled; - } - if (!experimentState.hasOwnProperty('state')) { - experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun; - } else { - processedExperiment.state = experimentState.state; - } - - return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => { - experimentState.state = processedExperiment.state = state; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); - - if (state === ExperimentState.Run) { - this.fireRunExperiment(processedExperiment); - } - return Promise.resolve(null); - }); - - }); - return Promise.all(promises).then(() => { - type ExperimentsClassification = { - experiments: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this.telemetryService.publicLog2<{ experiments: IExperiment[] }, ExperimentsClassification>('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) === -1) { - runExperimentIdsFromStorage.push(experiment.id); - } - - // Ensure we dont store duplicates - const distinctExperiments = distinct(runExperimentIdsFromStorage); - if (runExperimentIdsFromStorage.length !== distinctExperiments.length) { - this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.GLOBAL); - } - } - - private checkExperimentDependencies(experiment: IRawExperiment): boolean { - const experimentsPreviouslyRun = experiment.condition ? experiment.condition.experimentsPreviouslyRun : undefined; - if (experimentsPreviouslyRun) { - const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); - let includeCheck = true; - let excludeCheck = true; - const includes = experimentsPreviouslyRun.includes; - if (Array.isArray(includes)) { - includeCheck = runExperimentIdsFromStorage.some(x => includes.indexOf(x) > -1); - } - const excludes = experimentsPreviouslyRun.excludes; - if (includeCheck && Array.isArray(excludes)) { - excludeCheck = !runExperimentIdsFromStorage.some(x => excludes.indexOf(x) > -1); - } - if (!includeCheck || !excludeCheck) { - return false; - } - } - return true; - } - - private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise { - if (processedExperiment.state !== ExperimentState.Evaluating) { - return Promise.resolve(processedExperiment.state); - } - - if (!experiment.enabled) { - return Promise.resolve(ExperimentState.NoRun); - } - - const condition = experiment.condition; - if (!condition) { - return Promise.resolve(ExperimentState.Run); - } - - if (!this.checkExperimentDependencies(experiment)) { - return Promise.resolve(ExperimentState.NoRun); - } - - if (this.environmentService.appQuality === 'stable' && condition.insidersOnly === true) { - return Promise.resolve(ExperimentState.NoRun); - } - - const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL); - if ((condition.newUser === true && !isNewUser) - || (condition.newUser === false && isNewUser)) { - return Promise.resolve(ExperimentState.NoRun); - } - - if (typeof condition.displayLanguage === 'string') { - let localeToCheck = condition.displayLanguage.toLowerCase(); - let displayLanguage = language!.toLowerCase(); - - if (localeToCheck !== displayLanguage) { - const a = displayLanguage.indexOf('-'); - const b = localeToCheck.indexOf('-'); - if (a > -1) { - displayLanguage = displayLanguage.substr(0, a); - } - if (b > -1) { - localeToCheck = localeToCheck.substr(0, b); - } - if (displayLanguage !== localeToCheck) { - return Promise.resolve(ExperimentState.NoRun); - } - } - } - - if (!condition.userProbability) { - condition.userProbability = 1; - } - - let extensionsCheckPromise = Promise.resolve(true); - const installedExtensions = condition.installedExtensions; - if (installedExtensions) { - extensionsCheckPromise = this.extensionManagementService.getInstalled(ExtensionType.User).then(locals => { - let includesCheck = true; - let excludesCheck = true; - const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`); - if (Array.isArray(installedExtensions.includes) && installedExtensions.includes.length) { - const extensionIncludes = installedExtensions.includes.map(e => e.toLowerCase()); - includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1); - } - if (Array.isArray(installedExtensions.excludes) && installedExtensions.excludes.length) { - const extensionExcludes = installedExtensions.excludes.map(e => e.toLowerCase()); - excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1); - } - return includesCheck && excludesCheck; - }); - } - - const storageKey = 'experiments.' + experiment.id; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - - return extensionsCheckPromise.then(success => { - const fileEdits = condition.fileEdits; - if (!success || !fileEdits || typeof fileEdits.minEditCount !== 'number') { - const runExperiment = success && typeof condition.userProbability === 'number' && Math.random() < condition.userProbability; - return runExperiment ? ExperimentState.Run : ExperimentState.NoRun; - } - - experimentState.editCount = experimentState.editCount || 0; - if (experimentState.editCount >= fileEdits.minEditCount) { - return ExperimentState.Run; - } - - const onSaveHandler = this.textFileService.models.onModelsSaved(e => { - const date = new Date().toDateString(); - const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - if (latestExperimentState.state !== ExperimentState.Evaluating) { - onSaveHandler.dispose(); - return; - } - e.forEach(async event => { - if (event.kind !== StateChange.SAVED - || latestExperimentState.state !== ExperimentState.Evaluating - || date === latestExperimentState.lastEditedDate - || (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) - ) { - return; - } - let filePathCheck = true; - let workspaceCheck = true; - - if (typeof fileEdits.filePathPattern === 'string') { - filePathCheck = match(fileEdits.filePathPattern, event.resource.fsPath); - } - if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) { - const tags = await this.workspaceStatsService.getTags(); - workspaceCheck = !!tags && fileEdits.workspaceIncludes.some(x => !!tags[x]); - } - if (workspaceCheck && Array.isArray(fileEdits.workspaceExcludes) && fileEdits.workspaceExcludes.length) { - const tags = await this.workspaceStatsService.getTags(); - workspaceCheck = !!tags && !fileEdits.workspaceExcludes.some(x => !!tags[x]); - } - if (filePathCheck && workspaceCheck) { - latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1; - latestExperimentState.lastEditedDate = date; - this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); - } - }); - if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) { - processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun; - this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); - if (latestExperimentState.state === ExperimentState.Run && experiment.action && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) { - this.fireRunExperiment(processedExperiment); - } - } - }); - this._register(onSaveHandler); - return ExperimentState.Evaluating; - }); - } -} - - -function safeParse(text: string | undefined, defaultObject: any) { - try { - return text ? JSON.parse(text) || defaultObject : defaultObject; - } catch (e) { - return defaultObject; - } -} diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index f44b4d3904e56..ab7a75444b5b2 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExperimentActionType, ExperimentState, IExperiment } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { ExperimentService } from 'vs/workbench/contrib/experiments/electron-browser/experimentService'; +import { ExperimentActionType, ExperimentState, IExperiment, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; @@ -16,7 +15,7 @@ import { IExtensionEnablementService } from 'vs/workbench/services/extensionMana import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { Emitter } from 'vs/base/common/event'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { IURLService } from 'vs/platform/url/common/url'; import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts index 781fc5ab7c0fc..ff693a1f4e3ae 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts @@ -12,7 +12,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/electron-browser/experimentalPrompt'; +import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; import { ExperimentActionType, ExperimentState, IExperiment, IExperimentActionPromptProperties, IExperimentService, LocalizedPromptText } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index d29d891120b4a..17e83bed31931 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -50,25 +50,11 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; import { isUndefined } from 'vs/base/common/types'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { URI } from 'vs/base/common/uri'; -import { IWebviewService, Webview } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, Webview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; - -function renderBody(body: string): string { - const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://', 'vscode-resource://'); - return ` - - - - - - - - - ${body} - - `; -} +import { generateUuid } from 'vs/base/common/uuid'; +import { platform } from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; function removeEmbeddedSVGs(documentContent: string): string { const newDocument = new DOMParser().parseFromString(documentContent, 'text/html'); @@ -149,29 +135,33 @@ interface IActiveElement { focus(): void; } +interface IExtensionEditorTemplate { + iconContainer: HTMLElement; + icon: HTMLImageElement; + name: HTMLElement; + identifier: HTMLElement; + preview: HTMLElement; + builtin: HTMLElement; + license: HTMLElement; + publisher: HTMLElement; + installCount: HTMLElement; + rating: HTMLElement; + repository: HTMLElement; + description: HTMLElement; + extensionActionBar: ActionBar; + navbar: NavBar; + content: HTMLElement; + subtextContainer: HTMLElement; + subtext: HTMLElement; + ignoreActionbar: ActionBar; + header: HTMLElement; +} + export class ExtensionEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.extension'; - private iconContainer: HTMLElement; - private icon: HTMLImageElement; - private name: HTMLElement; - private identifier: HTMLElement; - private preview: HTMLElement; - private builtin: HTMLElement; - private license: HTMLElement; - private publisher: HTMLElement; - private installCount: HTMLElement; - private rating: HTMLElement; - private repository: HTMLElement; - private description: HTMLElement; - private extensionActionBar: ActionBar; - private navbar: NavBar; - private content: HTMLElement; - private subtextContainer: HTMLElement; - private subtext: HTMLElement; - private ignoreActionbar: ActionBar; - private header: HTMLElement; + private template: IExtensionEditorTemplate | undefined; private extensionReadme: Cache | null; private extensionChangelog: Cache | null; @@ -180,7 +170,7 @@ export class ExtensionEditor extends BaseEditor { private layoutParticipants: ILayoutParticipant[] = []; private readonly contentDisposables = this._register(new DisposableStore()); private readonly transientDisposables = this._register(new DisposableStore()); - private activeElement: IActiveElement | null; + private activeElement: IActiveElement | null = null; private editorLoadComplete: boolean = false; constructor( @@ -208,43 +198,43 @@ export class ExtensionEditor extends BaseEditor { const root = append(parent, $('.extension-editor')); root.tabIndex = 0; // this is required for the focus tracker on the editor root.style.outline = 'none'; - this.header = append(root, $('.header')); + const header = append(root, $('.header')); - this.iconContainer = append(this.header, $('.icon-container')); - this.icon = append(this.iconContainer, $('img.icon', { draggable: false })); + const iconContainer = append(header, $('.icon-container')); + const icon = append(iconContainer, $('img.icon', { draggable: false })); - const details = append(this.header, $('.details')); + const details = append(header, $('.details')); const title = append(details, $('.title')); - this.name = append(title, $('span.name.clickable', { title: localize('name', "Extension name") })); - this.identifier = append(title, $('span.identifier', { title: localize('extension id', "Extension identifier") })); + const name = append(title, $('span.name.clickable', { title: localize('name', "Extension name") })); + const identifier = append(title, $('span.identifier', { title: localize('extension id', "Extension identifier") })); - this.preview = append(title, $('span.preview', { title: localize('preview', "Preview") })); - this.preview.textContent = localize('preview', "Preview"); + const preview = append(title, $('span.preview', { title: localize('preview', "Preview") })); + preview.textContent = localize('preview', "Preview"); - this.builtin = append(title, $('span.builtin')); - this.builtin.textContent = localize('builtin', "Built-in"); + const builtin = append(title, $('span.builtin')); + builtin.textContent = localize('builtin', "Built-in"); const subtitle = append(details, $('.subtitle')); - this.publisher = append(subtitle, $('span.publisher.clickable', { title: localize('publisher', "Publisher name"), tabIndex: 0 })); + const publisher = append(subtitle, $('span.publisher.clickable', { title: localize('publisher', "Publisher name"), tabIndex: 0 })); - this.installCount = append(subtitle, $('span.install', { title: localize('install count', "Install count"), tabIndex: 0 })); + const installCount = append(subtitle, $('span.install', { title: localize('install count', "Install count"), tabIndex: 0 })); - this.rating = append(subtitle, $('span.rating.clickable', { title: localize('rating', "Rating"), tabIndex: 0 })); + const rating = append(subtitle, $('span.rating.clickable', { title: localize('rating', "Rating"), tabIndex: 0 })); - this.repository = append(subtitle, $('span.repository.clickable')); - this.repository.textContent = localize('repository', 'Repository'); - this.repository.style.display = 'none'; - this.repository.tabIndex = 0; + const repository = append(subtitle, $('span.repository.clickable')); + repository.textContent = localize('repository', 'Repository'); + repository.style.display = 'none'; + repository.tabIndex = 0; - this.license = append(subtitle, $('span.license.clickable')); - this.license.textContent = localize('license', 'License'); - this.license.style.display = 'none'; - this.license.tabIndex = 0; + const license = append(subtitle, $('span.license.clickable')); + license.textContent = localize('license', 'License'); + license.style.display = 'none'; + license.tabIndex = 0; - this.description = append(details, $('.description')); + const description = append(details, $('.description')); const extensionActions = append(details, $('.actions')); - this.extensionActionBar = new ActionBar(extensionActions, { + const extensionActionBar = this._register(new ActionBar(extensionActions, { animated: false, actionViewItemProvider: (action: Action) => { if (action instanceof ExtensionEditorDropDownAction) { @@ -252,29 +242,48 @@ export class ExtensionEditor extends BaseEditor { } return undefined; } - }); - - this.subtextContainer = append(details, $('.subtext-container')); - this.subtext = append(this.subtextContainer, $('.subtext')); - this.ignoreActionbar = new ActionBar(this.subtextContainer, { animated: false }); + })); - this._register(this.extensionActionBar); - this._register(this.ignoreActionbar); + const subtextContainer = append(details, $('.subtext-container')); + const subtext = append(subtextContainer, $('.subtext')); + const ignoreActionbar = this._register(new ActionBar(subtextContainer, { animated: false })); - this._register(Event.chain(this.extensionActionBar.onDidRun) + this._register(Event.chain(extensionActionBar.onDidRun) .map(({ error }) => error) .filter(error => !!error) .on(this.onError, this)); - this._register(Event.chain(this.ignoreActionbar.onDidRun) + this._register(Event.chain(ignoreActionbar.onDidRun) .map(({ error }) => error) .filter(error => !!error) .on(this.onError, this)); const body = append(root, $('.body')); - this.navbar = new NavBar(body); - - this.content = append(body, $('.content')); + const navbar = new NavBar(body); + + const content = append(body, $('.content')); + + this.template = { + builtin, + content, + description, + extensionActionBar, + header, + icon, + iconContainer, + identifier, + ignoreActionbar, + installCount, + license, + name, + navbar, + preview, + publisher, + rating, + repository, + subtext, + subtextContainer + }; } private onClick(element: HTMLElement, callback: () => void): IDisposable { @@ -292,6 +301,13 @@ export class ExtensionEditor extends BaseEditor { } async setInput(input: ExtensionsInput, options: EditorOptions, token: CancellationToken): Promise { + if (this.template) { + await this.updateTemplate(input, this.template); + } + return super.setInput(input, options, token); + } + + private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate): Promise { const runningExtensions = await this.extensionService.getExtensions(); const colorThemes = await this.workbenchThemeService.getColorThemes(); const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); @@ -306,18 +322,18 @@ export class ExtensionEditor extends BaseEditor { this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(token))); this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token))); - const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, this.iconContainer, true); - const onError = Event.once(domEvent(this.icon, 'error')); - onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables); - this.icon.src = extension.iconUrl; + const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, template.iconContainer, true); + const onError = Event.once(domEvent(template.icon, 'error')); + onError(() => template.icon.src = extension.iconUrlFallback, null, this.transientDisposables); + template.icon.src = extension.iconUrl; - this.name.textContent = extension.displayName; - this.identifier.textContent = extension.identifier.id; - this.preview.style.display = extension.preview ? 'inherit' : 'none'; - this.builtin.style.display = extension.type === ExtensionType.System ? 'inherit' : 'none'; + template.name.textContent = extension.displayName; + template.identifier.textContent = extension.identifier.id; + template.preview.style.display = extension.preview ? 'inherit' : 'none'; + template.builtin.style.display = extension.type === ExtensionType.System ? 'inherit' : 'none'; - this.publisher.textContent = extension.publisherDisplayName; - this.description.textContent = extension.description; + template.publisher.textContent = extension.publisherDisplayName; + template.description.textContent = extension.description; const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); let recommendationsData = {}; @@ -335,40 +351,40 @@ export class ExtensionEditor extends BaseEditor { */ this.telemetryService.publicLog('extensionGallery:openExtension', assign(extension.telemetryData, recommendationsData)); - toggleClass(this.name, 'clickable', !!extension.url); - toggleClass(this.publisher, 'clickable', !!extension.url); - toggleClass(this.rating, 'clickable', !!extension.url); + toggleClass(template.name, 'clickable', !!extension.url); + toggleClass(template.publisher, 'clickable', !!extension.url); + toggleClass(template.rating, 'clickable', !!extension.url); if (extension.url) { - this.transientDisposables.add(this.onClick(this.name, () => window.open(extension.url))); - this.transientDisposables.add(this.onClick(this.rating, () => window.open(`${extension.url}#review-details`))); - this.transientDisposables.add(this.onClick(this.publisher, () => { + this.transientDisposables.add(this.onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); + this.transientDisposables.add(this.onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}#review-details`)))); + this.transientDisposables.add(this.onClick(template.publisher, () => { this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) .then(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); })); if (extension.licenseUrl) { - this.transientDisposables.add(this.onClick(this.license, () => window.open(extension.licenseUrl))); - this.license.style.display = 'initial'; + this.transientDisposables.add(this.onClick(template.license, () => this.openerService.open(URI.parse(extension.licenseUrl!)))); + template.license.style.display = 'initial'; } else { - this.license.style.display = 'none'; + template.license.style.display = 'none'; } } else { - this.license.style.display = 'none'; + template.license.style.display = 'none'; } if (extension.repository) { - this.transientDisposables.add(this.onClick(this.repository, () => window.open(extension.repository))); - this.repository.style.display = 'initial'; + this.transientDisposables.add(this.onClick(template.repository, () => this.openerService.open(URI.parse(extension.repository!)))); + template.repository.style.display = 'initial'; } else { - this.repository.style.display = 'none'; + template.repository.style.display = 'none'; } const widgets = [ remoteBadge, - this.instantiationService.createInstance(InstallCountWidget, this.installCount, false), - this.instantiationService.createInstance(RatingsWidget, this.rating, false) + this.instantiationService.createInstance(InstallCountWidget, template.installCount, false), + this.instantiationService.createInstance(RatingsWidget, template.rating, false) ]; const reloadAction = this.instantiationService.createInstance(ReloadAction); const combinedInstallAction = this.instantiationService.createInstance(CombinedInstallAction); @@ -391,20 +407,20 @@ export class ExtensionEditor extends BaseEditor { const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); extensionContainers.extension = extension; - this.extensionActionBar.clear(); - this.extensionActionBar.push(actions, { icon: true, label: true }); + template.extensionActionBar.clear(); + template.extensionActionBar.push(actions, { icon: true, label: true }); for (const disposable of [...actions, ...widgets, extensionContainers]) { this.transientDisposables.add(disposable); } - this.setSubText(extension, reloadAction); - this.content.innerHTML = ''; // Clear content before setting navbar actions. + this.setSubText(extension, reloadAction, template); + template.content.innerHTML = ''; // Clear content before setting navbar actions. - this.navbar.clear(); - this.navbar.onChange(this.onNavbarChange.bind(this, extension), this, this.transientDisposables); + template.navbar.clear(); + template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables); if (extension.hasReadme()) { - this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); + template.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); } this.extensionManifest.get() .promise @@ -413,25 +429,23 @@ export class ExtensionEditor extends BaseEditor { combinedInstallAction.manifest = manifest; } if (extension.extensionPack.length) { - this.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); + template.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); } if (manifest && manifest.contributes) { - this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); + template.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")); + template.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")); + template.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); } this.editorLoadComplete = true; }); - - return super.setInput(input, options, token); } - private setSubText(extension: IExtension, reloadAction: ReloadAction): void { - hide(this.subtextContainer); + private setSubText(extension: IExtension, reloadAction: ReloadAction, template: IExtensionEditorTemplate): void { + hide(template.subtextContainer); const ignoreAction = this.instantiationService.createInstance(IgnoreExtensionRecommendationAction); const undoIgnoreAction = this.instantiationService.createInstance(UndoIgnoreExtensionRecommendationAction); @@ -440,23 +454,23 @@ export class ExtensionEditor extends BaseEditor { ignoreAction.enabled = false; undoIgnoreAction.enabled = false; - this.ignoreActionbar.clear(); - this.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true }); + template.ignoreActionbar.clear(); + template.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true }); this.transientDisposables.add(ignoreAction); this.transientDisposables.add(undoIgnoreAction); const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); if (extRecommendations[extension.identifier.id.toLowerCase()]) { ignoreAction.enabled = true; - this.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; - show(this.subtextContainer); + template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; + show(template.subtextContainer); } else if (this.extensionTipsService.getAllIgnoredRecommendations().global.indexOf(extension.identifier.id.toLowerCase()) !== -1) { undoIgnoreAction.enabled = true; - this.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); - show(this.subtextContainer); + template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); + show(template.subtextContainer); } else { - this.subtext.textContent = ''; + template.subtext.textContent = ''; } this.extensionTipsService.onRecommendationChange(change => { @@ -466,28 +480,28 @@ export class ExtensionEditor extends BaseEditor { const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); if (extRecommendations[extension.identifier.id.toLowerCase()]) { ignoreAction.enabled = true; - this.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; + template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; } } else { undoIgnoreAction.enabled = true; ignoreAction.enabled = false; - this.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); + template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); } } }); this.transientDisposables.add(reloadAction.onDidChange(e => { if (e.tooltip) { - this.subtext.textContent = reloadAction.tooltip; - show(this.subtextContainer); + template.subtext.textContent = reloadAction.tooltip; + show(template.subtextContainer); ignoreAction.enabled = false; undoIgnoreAction.enabled = false; } if (e.enabled === true) { - show(this.subtextContainer); + show(template.subtextContainer); } if (e.enabled === false) { - hide(this.subtextContainer); + hide(template.subtextContainer); } })); } @@ -511,7 +525,13 @@ export class ExtensionEditor extends BaseEditor { } } - private onNavbarChange(extension: IExtension, { id, focus }: { id: string, focus: boolean }): void { + runFindAction(previous: boolean): void { + if (this.activeElement && (this.activeElement).runFindAction) { + (this.activeElement).runFindAction(previous); + } + } + + private onNavbarChange(extension: IExtension, { id, focus }: { id: string | null, focus: boolean }, template: IExtensionEditorTemplate): void { if (this.editorLoadComplete) { /* __GDPR__ "extensionEditor:navbarChange" : { @@ -525,45 +545,42 @@ export class ExtensionEditor extends BaseEditor { } this.contentDisposables.clear(); - this.content.innerHTML = ''; + template.content.innerHTML = ''; this.activeElement = null; - this.open(id, extension) - .then(activeElement => { - this.activeElement = activeElement; - if (focus) { - this.focus(); - } - }); + if (id) { + this.open(id, extension, template) + .then(activeElement => { + this.activeElement = activeElement; + if (focus) { + this.focus(); + } + }); + } } - private open(id: string, extension: IExtension): Promise { + private open(id: string, extension: IExtension, template: IExtensionEditorTemplate): Promise { switch (id) { - case NavbarSection.Readme: return this.openReadme(); - 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); + case NavbarSection.Readme: return this.openReadme(template); + case NavbarSection.Contributions: return this.openContributions(template); + case NavbarSection.Changelog: return this.openChangelog(template); + case NavbarSection.Dependencies: return this.openDependencies(extension, template); + case NavbarSection.ExtensionPack: return this.openExtensionPack(extension, template); } return Promise.resolve(null); } - private openMarkdown(cacheResult: CacheResult, noContentCopy: string): Promise { - return this.loadContents(() => cacheResult) + private openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate): Promise { + return this.loadContents(() => cacheResult, template) .then(marked.parse) - .then(renderBody) + .then(content => this.renderBody(content)) .then(removeEmbeddedSVGs) .then(body => { const webviewElement = this.webviewService.createWebview('extensionEditor', { enableFindWidget: true, }, - { - svgWhiteList: this.extensionsWorkbenchService.allowedBadgeProviders, - localResourceRoots: [ - URI.parse(require.toUrl('./media')) - ] - }); - webviewElement.mountTo(this.content); + {}); + webviewElement.mountTo(template.content); this.contentDisposables.add(webviewElement.onDidFocus(() => this.fireOnDidFocus())); const removeLayoutParticipant = arrays.insert(this.layoutParticipants, webviewElement); this.contentDisposables.add(toDisposable(removeLayoutParticipant)); @@ -582,23 +599,211 @@ export class ExtensionEditor extends BaseEditor { return webviewElement; }) .then(undefined, () => { - const p = append(this.content, $('p.nocontent')); + const p = append(template.content, $('p.nocontent')); p.textContent = noContentCopy; return p; }); } - private openReadme(): Promise { - return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available.")); + private async renderBody(body: string): Promise { + const nonce = generateUuid(); + return ` + + + + + + + + + ${body} + + `; + } + + private openReadme(template: IExtensionEditorTemplate): Promise { + return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template); } - private openChangelog(): Promise { - return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available.")); + private openChangelog(template: IExtensionEditorTemplate): Promise { + return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template); } - private openContributions(): Promise { + private openContributions(template: IExtensionEditorTemplate): Promise { const content = $('div', { class: 'subcontent', tabindex: '0' }); - return this.loadContents(() => this.extensionManifest!.get()) + return this.loadContents(() => this.extensionManifest!.get(), template) .then(manifest => { if (!manifest) { return content; @@ -629,28 +834,28 @@ export class ExtensionEditor extends BaseEditor { const isEmpty = !renders.some(x => x); if (isEmpty) { append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); - append(this.content, content); + append(template.content, content); } else { - append(this.content, scrollableContent.getDomNode()); + append(template.content, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); } return content; }, () => { append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); - append(this.content, content); + append(template.content, content); return content; }); } - private openDependencies(extension: IExtension): Promise { + private openDependencies(extension: IExtension, template: IExtensionEditorTemplate): Promise { if (arrays.isFalsyOrEmpty(extension.dependencies)) { - append(this.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); - return Promise.resolve(this.content); + append(template.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); + return Promise.resolve(template.content); } const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); - append(this.content, scrollableContent.getDomNode()); + append(template.content, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension, null, extension => extension.dependencies || [], this.extensionsWorkbenchService), content); @@ -667,10 +872,10 @@ export class ExtensionEditor extends BaseEditor { return Promise.resolve({ focus() { dependenciesTree.domFocus(); } }); } - private openExtensionPack(extension: IExtension): Promise { + private openExtensionPack(extension: IExtension, template: IExtensionEditorTemplate): Promise { const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); - append(this.content, scrollableContent.getDomNode()); + append(template.content, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); const extensionsPackTree = this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension, null, extension => extension.extensionPack || [], this.extensionsWorkbenchService), content); @@ -1076,7 +1281,7 @@ export class ExtensionEditor extends BaseEditor { private resolveKeybinding(rawKeyBinding: IKeyBinding): ResolvedKeybinding | null { let key: string | undefined; - switch (process.platform) { + switch (platform) { case 'win32': key = rawKeyBinding.win; break; case 'linux': key = rawKeyBinding.linux; break; case 'darwin': key = rawKeyBinding.mac; break; @@ -1090,11 +1295,11 @@ export class ExtensionEditor extends BaseEditor { return null; } - private loadContents(loadingTask: () => CacheResult): Promise { - addClass(this.content, 'loading'); + private loadContents(loadingTask: () => CacheResult, template: IExtensionEditorTemplate): Promise { + addClass(template.content, 'loading'); const result = loadingTask(); - const onDone = () => removeClass(this.content, 'loading'); + const onDone = () => removeClass(template.content, 'loading'); result.promise.then(onDone, onDone); this.contentDisposables.add(toDisposable(() => result.dispose())); @@ -1115,28 +1320,66 @@ export class ExtensionEditor extends BaseEditor { } } +const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), ContextKeyExpr.not('editorFocus')); class ShowExtensionEditorFindCommand extends Command { public runCommand(accessor: ServicesAccessor, args: any): void { - const extensionEditor = this.getExtensionEditor(accessor); + const extensionEditor = getExtensionEditor(accessor); if (extensionEditor) { extensionEditor.showFind(); } } +} +(new ShowExtensionEditorFindCommand({ + id: 'editor.action.extensioneditor.showfind', + precondition: contextKeyExpr, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + weight: KeybindingWeight.EditorContrib + } +})).register(); + +class StartExtensionEditorFindNextCommand extends Command { + public runCommand(accessor: ServicesAccessor, args: any): void { + const extensionEditor = getExtensionEditor(accessor); + if (extensionEditor) { + extensionEditor.runFindAction(false); + } + } +} +(new StartExtensionEditorFindNextCommand({ + id: 'editor.action.extensioneditor.findNext', + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } +})).register(); - private getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null { - const activeControl = accessor.get(IEditorService).activeControl as ExtensionEditor; - if (activeControl instanceof ExtensionEditor) { - return activeControl; +class StartExtensionEditorFindPreviousCommand extends Command { + public runCommand(accessor: ServicesAccessor, args: any): void { + const extensionEditor = getExtensionEditor(accessor); + if (extensionEditor) { + extensionEditor.runFindAction(true); } - return null; } } -const showCommand = new ShowExtensionEditorFindCommand({ - id: 'editor.action.extensioneditor.showfind', - precondition: ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), ContextKeyExpr.not('editorFocus')), +(new StartExtensionEditorFindPreviousCommand({ + id: 'editor.action.extensioneditor.findPrevious', + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + primary: KeyMod.Shift | KeyCode.Enter, weight: KeybindingWeight.EditorContrib } -}); -showCommand.register(); +})).register(); + +function getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null { + const activeControl = accessor.get(IEditorService).activeControl as ExtensionEditor; + if (activeControl instanceof ExtensionEditor) { + return activeControl; + } + return null; +} diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts similarity index 93% rename from src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts rename to src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts index b424211b797c3..3e7884ae1f7ae 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts @@ -14,7 +14,6 @@ import { IExtensionTipsService, ExtensionRecommendationReason, IExtensionsConfig import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModel } from 'vs/editor/common/model'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import product from 'vs/platform/product/node/product'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import Severity from 'vs/base/common/severity'; @@ -23,13 +22,9 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionsConfiguration, ConfigurationKey, ShowRecommendationsOnlyOnDemandKey, IExtensionsViewlet, IExtensionsWorkbenchService, EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import * as pfs from 'vs/base/node/pfs'; -import * as os from 'os'; import { flatten, distinct, shuffle, coalesce } from 'vs/base/common/arrays'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { guessMimeTypes, MIME_UNKNOWN } from 'vs/base/common/mime'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { getHashedRemotesFromUri } from 'vs/workbench/contrib/stats/electron-browser/workspaceStats'; import { IRequestService, asJson } from 'vs/platform/request/common/request'; import { isNumber } from 'vs/base/common/types'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -42,9 +37,12 @@ import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/wo import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { extname } from 'vs/base/common/resources'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IExeBasedExtensionTip } from 'vs/platform/product/common/product'; +import { IExeBasedExtensionTip, IProductService } from 'vs/platform/product/common/product'; import { timeout } from 'vs/base/common/async'; +import { IWorkspaceStatsService } from 'vs/workbench/contrib/stats/common/workspaceStats'; +import { setImmediate, isWeb } from 'vs/base/common/platform'; +import { platform, env as processEnv } from 'vs/base/common/process'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const milliSecondsInADay = 1000 * 60 * 60 * 24; const choiceNever = localize('neverShowAgain', "Don't Show Again"); @@ -101,7 +99,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IExtensionService private readonly extensionService: IExtensionService, @IRequestService private readonly requestService: IRequestService, @IViewletService private readonly viewletService: IViewletService, @@ -109,7 +107,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, @IExperimentService private readonly experimentService: IExperimentService, - @ITextFileService private readonly textFileService: ITextFileService + @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService, + @IProductService private readonly productService: IProductService ) { super(); @@ -117,8 +116,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return; } - if (product.extensionsGallery && product.extensionsGallery.recommendationsUrl) { - this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl; + if (this.productService.extensionsGallery && this.productService.extensionsGallery.recommendationsUrl) { + this._extensionsRecommendationsUrl = this.productService.extensionsGallery.recommendationsUrl; } this.sessionSeed = +new Date(); @@ -244,7 +243,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } getKeymapRecommendations(): IExtensionRecommendation[] { - return (product.keymapExtensionTips || []) + return (this.productService.keymapExtensionTips || []) .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)) .map(extensionId => ({ extensionId, sources: ['application'] })); } @@ -521,8 +520,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return false; } - const id = recommendationsToSuggest[0]; - const tip = this._importantExeBasedRecommendations[id]; + const extensionId = recommendationsToSuggest[0]; + const tip = this._importantExeBasedRecommendations[extensionId]; const message = localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.friendlyName!, tip.exeFriendlyName || basename(tip.windowsPath!)); this.notificationService.prompt(Severity.Info, message, @@ -535,8 +534,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'install', extensionId: name }); - this.instantiationService.createInstance(InstallRecommendedExtensionAction, id).run(); + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'install', extensionId }); + this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionId).run(); } }, { label: localize('showRecommendations', "Show Recommendations"), @@ -547,7 +546,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'show', extensionId: name }); + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'show', extensionId }); const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); recommendationsAction.run(); @@ -557,14 +556,14 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe label: choiceNever, isSecondary: true, run: () => { - this.addToImportantRecommendationsIgnore(id); + this.addToImportantRecommendationsIgnore(extensionId); /* __GDPR__ "exeExtensionRecommendations:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name }); + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId }); this.notificationService.prompt( Severity.Info, localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"), @@ -587,7 +586,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name }); + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'cancelled', extensionId }); } } ); @@ -601,10 +600,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return Object.keys(this._fileBasedRecommendations) .sort((a, b) => { if (this._fileBasedRecommendations[a].recommendedTime === this._fileBasedRecommendations[b].recommendedTime) { - if (!product.extensionImportantTips || caseInsensitiveGet(product.extensionImportantTips, a)) { + if (!this.productService.extensionImportantTips || caseInsensitiveGet(this.productService.extensionImportantTips, a)) { return -1; } - if (caseInsensitiveGet(product.extensionImportantTips, b)) { + if (caseInsensitiveGet(this.productService.extensionImportantTips, b)) { return 1; } } @@ -615,11 +614,11 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } /** - * Parse all file based recommendations from product.extensionTips - * Retire existing recommendations if they are older than a week or are not part of product.extensionTips anymore + * Parse all file based recommendations from this.productService.extensionTips + * Retire existing recommendations if they are older than a week or are not part of this.productService.extensionTips anymore */ private fetchFileBasedRecommendations() { - const extensionTips = product.extensionTips; + const extensionTips = this.productService.extensionTips; if (!extensionTips) { return; } @@ -636,7 +635,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } }); - forEach(product.extensionImportantTips, entry => { + forEach(this.productService.extensionImportantTips, entry => { let { key: id, value } = entry; const { pattern } = value; let ids = this._availableRecommendations[pattern]; @@ -698,7 +697,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe let { key: pattern, value: ids } = entry; if (match(pattern, model.uri.toString())) { for (let id of ids) { - if (caseInsensitiveGet(product.extensionImportantTips, id)) { + if (caseInsensitiveGet(this.productService.extensionImportantTips, id)) { recommendationsToSuggest.push(id); } const filedBasedRecommendation = this._fileBasedRecommendations[id.toLowerCase()] || { recommendedTime: now, sources: [] }; @@ -752,7 +751,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } const id = recommendationsToSuggest[0]; - const entry = caseInsensitiveGet(product.extensionImportantTips, id); + const entry = caseInsensitiveGet(this.productService.extensionImportantTips, id); if (!entry) { return false; } @@ -982,14 +981,16 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } /** - * If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations + * If user has any of the tools listed in this.productService.exeBasedExtensionTips, fetch corresponding recommendations */ - private fetchExecutableRecommendations(important: boolean): Promise { - const homeDir = os.homedir(); - let foundExecutables: Set = new Set(); + private async fetchExecutableRecommendations(important: boolean): Promise { + if (isWeb) { + return; + } - let findExecutable = (exeName: string, tip: IExeBasedExtensionTip, path: string) => { - return pfs.fileExists(path).then(exists => { + const foundExecutables: Set = new Set(); + const findExecutable = (exeName: string, tip: IExeBasedExtensionTip, path: string) => { + return this.fileService.exists(URI.file(path)).then(exists => { if (exists && !foundExecutables.has(exeName)) { foundExecutables.add(exeName); (tip['recommendations'] || []).forEach(extensionId => { @@ -1004,9 +1005,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); }; - let promises: Promise[] = []; + const promises: Promise[] = []; // Loop through recommended extensions - forEach(product.exeBasedExtensionTips, entry => { + forEach(this.productService.exeBasedExtensionTips, entry => { if (typeof entry.value !== 'object' || !Array.isArray(entry.value['recommendations'])) { return; } @@ -1014,24 +1015,24 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return; } const exeName = entry.key; - if (process.platform === 'win32') { + if (platform === 'win32') { let windowsPath = entry.value['windowsPath']; if (!windowsPath || typeof windowsPath !== 'string') { return; } - windowsPath = windowsPath.replace('%USERPROFILE%', process.env['USERPROFILE']!) - .replace('%ProgramFiles(x86)%', process.env['ProgramFiles(x86)']!) - .replace('%ProgramFiles%', process.env['ProgramFiles']!) - .replace('%APPDATA%', process.env['APPDATA']!) - .replace('%WINDIR%', process.env['WINDIR']!); + windowsPath = windowsPath.replace('%USERPROFILE%', processEnv['USERPROFILE']!) + .replace('%ProgramFiles(x86)%', processEnv['ProgramFiles(x86)']!) + .replace('%ProgramFiles%', processEnv['ProgramFiles']!) + .replace('%APPDATA%', processEnv['APPDATA']!) + .replace('%WINDIR%', processEnv['WINDIR']!); promises.push(findExecutable(exeName, entry.value, windowsPath)); } else { promises.push(findExecutable(exeName, entry.value, join('/usr/local/bin', exeName))); - promises.push(findExecutable(exeName, entry.value, join(homeDir, exeName))); + promises.push(findExecutable(exeName, entry.value, join(this.environmentService.userHome, exeName))); } }); - return Promise.all(promises).then(() => undefined); + await Promise.all(promises); } /** @@ -1078,7 +1079,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe const storageKey = 'extensionsAssistant/dynamicWorkspaceRecommendations'; const workspaceUri = this.contextService.getWorkspace().folders[0].uri; - return Promise.all([getHashedRemotesFromUri(workspaceUri, this.fileService, this.textFileService, false), getHashedRemotesFromUri(workspaceUri, this.fileService, this.textFileService, true)]).then(([hashedRemotes1, hashedRemotes2]) => { + return Promise.all([this.workspaceStatsService.getHashedRemotesFromUri(workspaceUri, false), this.workspaceStatsService.getHashedRemotesFromUri(workspaceUri, true)]).then(([hashedRemotes1, hashedRemotes2]) => { const hashedRemotes = (hashedRemotes1 || []).concat(hashedRemotes2 || []); if (!hashedRemotes.length) { return undefined; @@ -1143,4 +1144,5 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private isExtensionAllowedToBeRecommended(id: string): boolean { return this._allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1; } + } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 32bd703fba030..b7bed063f07e2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionsLabel, ExtensionsChannelId, PreferencesLabel, IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/contrib/output/common/output'; @@ -45,9 +45,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { RemoteExtensionsInstaller } from 'vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller'; +import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); +registerSingleton(IExtensionTipsService, ExtensionTipsService); Registry.as(OutputExtensions.OutputChannels) .registerChannel({ id: ExtensionsChannelId, label: ExtensionsLabel, log: false }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index b598b94ea4bff..53632cb694afa 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -3022,6 +3022,8 @@ interface IExtensionPickItem extends IQuickPickItem { export class InstallLocalExtensionsInRemoteAction extends Action { + private extensions: IExtension[] | undefined = undefined; + constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @@ -3034,32 +3036,49 @@ export class InstallLocalExtensionsInRemoteAction extends Action { ) { super('workbench.extensions.actions.installLocalExtensionsInRemote'); this.update(); - this._register(this.extensionsWorkbenchService.onChange(() => this.update())); + this.extensionsWorkbenchService.queryLocal().then(() => this.updateExtensions()); + this._register(this.extensionsWorkbenchService.onChange(() => { + if (this.extensions) { + this.updateExtensions(); + } + })); } get label(): string { - return this.extensionManagementServerService.remoteExtensionManagementServer ? - localize('install local extensions', "Install Local Extensions in {0}...", this.extensionManagementServerService.remoteExtensionManagementServer.label) : ''; + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + return localize('select and install local extensions', "Install Local Extensions in {0}...", this.extensionManagementServerService.remoteExtensionManagementServer.label); + } + return ''; + } + + private updateExtensions(): void { + this.extensions = this.extensionsWorkbenchService.local; + this.update(); } private update(): void { - this.enabled = this.getLocalExtensionsToInstall().length > 0; + this.enabled = !!this.extensions && this.getExtensionsToInstall(this.extensions).length > 0; + this.tooltip = this.label; } - private getLocalExtensionsToInstall(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(extension => { + async run(): Promise { + return this.selectAndInstallLocalExtensions(); + } + + private async queryExtensionsToInstall(): Promise { + const local = await this.extensionsWorkbenchService.queryLocal(); + return this.getExtensionsToInstall(local); + } + + private getExtensionsToInstall(local: IExtension[]): IExtension[] { + return local.filter(extension => { const action = this.instantiationService.createInstance(RemoteInstallAction); action.extension = extension; return action.enabled; }); } - async run(): Promise { - this.selectAndInstallLocalExtensions(); - return Promise.resolve(); - } - - private selectAndInstallLocalExtensions(): void { + private async selectAndInstallLocalExtensions(): Promise { const quickPick = this.quickInputService.createQuickPick(); quickPick.busy = true; const disposable = quickPick.onDidAccept(() => { @@ -3069,7 +3088,7 @@ export class InstallLocalExtensionsInRemoteAction extends Action { this.onDidAccept(quickPick.selectedItems); }); quickPick.show(); - const localExtensionsToInstall = this.getLocalExtensionsToInstall(); + const localExtensionsToInstall = await this.queryExtensionsToInstall(); quickPick.busy = false; if (localExtensionsToInstall.length) { quickPick.title = localize('install local extensions title', "Install Local Extensions in {0}", this.extensionManagementServerService.remoteExtensionManagementServer!.label); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 8747ad8ca519b..aeb50c75c645d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -156,7 +156,7 @@ export class UnknownExtensionRenderer implements IListRenderer { - return this.extensionsWorkdbenchService.open(this.extensionData.extension, sideByside); + if (this._extensionData) { + return this.extensionsWorkdbenchService.open(this._extensionData.extension, sideByside); + } + return Promise.resolve(); } } @@ -263,4 +262,4 @@ export class ExtensionData implements IExtensionData { } return null; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 50e67a2a8880d..d9aef35dae5be 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -22,7 +22,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, AutoUpdate import { ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, InstallLocalExtensionsInRemoteAction + EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -319,7 +319,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensionsViewlet { - private onSearchChange: EventOf; + private readonly _onSearchChange: Emitter = this._register(new Emitter()); + private readonly onSearchChange: EventOf = this._onSearchChange.event; private nonEmptyWorkspaceContextKey: IContextKey; private defaultViewsContextKey: IContextKey; private searchMarketplaceExtensionsContextKey: IContextKey; @@ -333,12 +334,10 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private defaultRecommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; - private root: HTMLElement; - - private searchBox: SuggestEnabledInput; - private extensionsBox: HTMLElement; - private primaryActions: IAction[]; - private secondaryActions: IAction[] | null; + private root: HTMLElement | undefined; + private searchBox: SuggestEnabledInput | undefined; + private primaryActions: IAction[] | undefined; + private secondaryActions: IAction[] | null = null; private readonly searchViewletState: MementoObject; constructor( @@ -348,7 +347,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -418,32 +416,35 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this._register(attachSuggestEnabledInputBoxStyler(this.searchBox, this.themeService)); - const _searchChange = new Emitter(); - this.onSearchChange = _searchChange.event; this._register(this.searchBox.onInputDidChange(() => { this.triggerSearch(); - _searchChange.fire(this.searchBox.getValue()); + this._onSearchChange.fire(this.searchBox!.getValue()); }, this)); this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); this._register(this.onDidChangeVisibility(visible => { if (visible) { - this.searchBox.focus(); + this.searchBox!.focus(); } })); - this.extensionsBox = append(this.root, $('.extensions')); - super.create(this.extensionsBox); + super.create(append(this.root, $('.extensions'))); } focus(): void { - this.searchBox.focus(); + if (this.searchBox) { + this.searchBox.focus(); + } } layout(dimension: Dimension): void { - toggleClass(this.root, 'narrow', dimension.width <= 300); - this.searchBox.layout({ height: 20, width: dimension.width - 34 }); + if (this.root) { + toggleClass(this.root, 'narrow', dimension.width <= 300); + } + if (this.searchBox) { + this.searchBox.layout({ height: 20, width: dimension.width - 34 }); + } super.layout(new Dimension(dimension.width, dimension.height - 38)); } @@ -454,7 +455,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio getActions(): IAction[] { if (!this.primaryActions) { this.primaryActions = [ - this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox.getValue()) + this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : '') ]; } return this.primaryActions; @@ -478,7 +479,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL), ...(this.configurationService.getValue(AutoUpdateConfigurationKey) ? [this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)] : [this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)]), this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL), - ...(this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer ? [this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)] : []), new Separator(), this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL), this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL) @@ -489,22 +489,24 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio } search(value: string): void { - const event = new Event('input', { bubbles: true }) as SearchInputEvent; - event.immediate = true; + if (this.searchBox) { + const event = new Event('input', { bubbles: true }) as SearchInputEvent; + event.immediate = true; - this.searchBox.setValue(value); + this.searchBox.setValue(value); + } } - private triggerSearch(immediate = false): void { - this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.getValue() ? 0 : 500).then(undefined, err => this.onError(err)); + private triggerSearch(): void { + this.searchDelayer.trigger(() => this.doSearch(), this.searchBox && this.searchBox.getValue() ? 500 : 0).then(undefined, err => this.onError(err)); } private normalizedQuery(): string { - return this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:'); + return this.searchBox ? this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:') : ''; } protected saveState(): void { - const value = this.searchBox.getValue(); + const value = this.searchBox ? this.searchBox.getValue() : ''; if (ExtensionsListView.isLocalExtensionsQuery(value)) { this.searchViewletState['query.value'] = value; } else { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 5f905a190e230..9eabbf3220f07 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -14,11 +14,11 @@ import { IExtensionManagementServer, IExtensionManagementServerService, IExtensi import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { append, $, toggleClass } from 'vs/base/browser/dom'; +import { append, $, toggleClass, addClass } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList'; -import { IExtension, IExtensionsWorkbenchService, ExtensionState } from '../common/extensions'; -import { Query } from '../common/extensionQuery'; +import { IExtension, IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; +import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; @@ -27,8 +27,8 @@ import { OpenGlobalSettingsAction } from 'vs/workbench/contrib/preferences/brows import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -73,15 +73,16 @@ class ExtensionListViewWarning extends Error { } export class ExtensionsListView extends ViewletPanel { - private readonly server: IExtensionManagementServer | undefined; - private messageContainer: HTMLElement; - private messageSeverityIcon: HTMLElement; - private messageBox: HTMLElement; - private extensionsList: HTMLElement; - private badge: CountBadge; - protected badgeContainer: HTMLElement; - private list: WorkbenchPagedList | null; - private queryRequest: { query: string, request: CancelablePromise> } | null; + protected readonly server: IExtensionManagementServer | undefined; + private bodyTemplate: { + messageContainer: HTMLElement; + messageSeverityIcon: HTMLElement; + messageBox: HTMLElement; + extensionsList: HTMLElement; + } | undefined; + private badge: CountBadge | undefined; + private list: WorkbenchPagedList | null = null; + private queryRequest: { query: string, request: CancelablePromise> } | null = null; constructor( options: ExtensionsListViewOptions, @@ -103,31 +104,27 @@ export class ExtensionsListView extends ViewletPanel { @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService); this.server = options.server; } protected renderHeader(container: HTMLElement): void { - this.renderHeaderTitle(container); - } - - renderHeaderTitle(container: HTMLElement): void { - super.renderHeaderTitle(container, this.title); + addClass(container, 'extension-view-header'); + super.renderHeader(container); - this.badgeContainer = append(container, $('.count-badge-wrapper')); - this.badge = new CountBadge(this.badgeContainer); + this.badge = new CountBadge(append(container, $('.count-badge-wrapper'))); this._register(attachBadgeStyler(this.badge, this.themeService)); } renderBody(container: HTMLElement): void { - this.extensionsList = append(container, $('.extensions-list')); - this.messageContainer = append(container, $('.message-container')); - this.messageSeverityIcon = append(this.messageContainer, $('')); - this.messageBox = append(this.messageContainer, $('.message')); + const extensionsList = append(container, $('.extensions-list')); + const messageContainer = append(container, $('.message-container')); + const messageSeverityIcon = append(messageContainer, $('')); + const messageBox = append(messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState); - this.list = this.instantiationService.createInstance(WorkbenchPagedList, this.extensionsList, delegate, [renderer], { + this.list = this.instantiationService.createInstance(WorkbenchPagedList, extensionsList, delegate, [renderer], { ariaLabel: localize('extensions', "Extensions"), multipleSelectionSupport: false, setRowLineHeight: false, @@ -147,10 +144,19 @@ export class ExtensionsListView extends ViewletPanel { .map(e => e.elements[0]) .filter(e => !!e) .on(this.pin, this)); + + this.bodyTemplate = { + extensionsList, + messageBox, + messageContainer, + messageSeverityIcon + }; } protected layoutBody(height: number, width: number): void { - this.extensionsList.style.height = height + 'px'; + if (this.bodyTemplate) { + this.bodyTemplate.extensionsList.style.height = height + 'px'; + } if (this.list) { this.list.layout(height, width); } @@ -482,7 +488,7 @@ export class ExtensionsListView extends ViewletPanel { } - private _searchExperiments: Promise; + private _searchExperiments: Promise | undefined; private getSearchExperiments(): Promise { if (!this._searchExperiments) { this._searchExperiments = this.experimentService.getExperimentsByType(ExperimentActionType.ExtensionSearchResults); @@ -693,24 +699,27 @@ export class ExtensionsListView extends ViewletPanel { this.list.scrollTop = 0; const count = this.count(); - toggleClass(this.extensionsList, 'hidden', count === 0); - toggleClass(this.messageContainer, 'hidden', count > 0); - this.badge.setCount(count); + if (this.bodyTemplate && this.badge) { + + toggleClass(this.bodyTemplate.extensionsList, 'hidden', count === 0); + toggleClass(this.bodyTemplate.messageContainer, 'hidden', count > 0); + this.badge.setCount(count); - if (count === 0 && this.isBodyVisible()) { - if (error) { - if (error instanceof ExtensionListViewWarning) { - this.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning); - this.messageBox.textContent = getErrorMessage(error); + if (count === 0 && this.isBodyVisible()) { + if (error) { + if (error instanceof ExtensionListViewWarning) { + this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning); + this.bodyTemplate.messageBox.textContent = getErrorMessage(error); + } else { + this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Error); + this.bodyTemplate.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error)); + } } else { - this.messageSeverityIcon.className = SeverityIcon.className(Severity.Error); - this.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error)); + this.bodyTemplate.messageSeverityIcon.className = ''; + this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found."); } - } else { - this.messageSeverityIcon.className = ''; - this.messageBox.textContent = localize('no extensions found', "No extensions found."); + alert(this.bodyTemplate.messageBox.textContent); } - alert(this.messageBox.textContent); } } } @@ -850,7 +859,7 @@ export class ServerExtensionsView extends ExtensionsListView { @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService ) { options.server = server; super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService); @@ -864,6 +873,15 @@ export class ServerExtensionsView extends ExtensionsListView { } return super.show(query.trim()); } + + getActions(): IAction[] { + if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.server) { + const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)); + installLocalExtensionsInRemoteAction.class = 'octicon octicon-cloud-download'; + return [installLocalExtensionsInRemoteAction]; + } + return []; + } } export class EnabledExtensionsView extends ExtensionsListView { @@ -943,7 +961,7 @@ export class RecommendedExtensionsView extends ExtensionsListView { export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: InstallWorkspaceRecommendedExtensionsAction; + private installAllAction: InstallWorkspaceRecommendedExtensionsAction | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); @@ -953,25 +971,15 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this._register(this.contextService.onDidChangeWorkbenchState(() => this.update())); } - renderHeader(container: HTMLElement): void { - super.renderHeader(container); - - const listActionBar = $('.list-actionbar-container'); - container.insertBefore(listActionBar, this.badgeContainer); - - const actionbar = this._register(new ActionBar(listActionBar, { - animated: false - })); - actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); + getActions(): IAction[] { + if (!this.installAllAction) { + this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL, [])); + this.installAllAction.class = 'octicon octicon-cloud-download'; + } - this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL, [])); const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); - - this.installAllAction.class = 'octicon octicon-cloud-download'; configureWorkspaceFolderAction.class = 'octicon octicon-pencil'; - - actionbar.push([this.installAllAction], { icon: true, label: false }); - actionbar.push([configureWorkspaceFolderAction], { icon: true, label: false }); + return [this.installAllAction, configureWorkspaceFolderAction]; } async show(query: string): Promise> { @@ -986,9 +994,11 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this.setRecommendationsToInstall(); } - private setRecommendationsToInstall(): Promise { - return this.getRecommendationsToInstall() - .then(recommendations => { this.installAllAction.recommendations = recommendations; }); + private async setRecommendationsToInstall(): Promise { + const recommendations = await this.getRecommendationsToInstall(); + if (this.installAllAction) { + this.installAllAction.recommendations = recommendations; + } } private getRecommendationsToInstall(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index e27cc72e7d173..05741f72a6216 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -18,9 +18,9 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } + private _extension: IExtension | null = null; + get extension(): IExtension | null { return this._extension; } + set extension(extension: IExtension | null) { this._extension = extension; this.update(); } update(): void { this.render(); } abstract render(): void; } @@ -183,7 +183,7 @@ export class RecommendationWidget extends ExtensionWidget { private element?: HTMLElement; private readonly disposables = this._register(new DisposableStore()); - private _tooltip: string; + private _tooltip: string = ''; get tooltip(): string { return this._tooltip; } set tooltip(tooltip: string) { if (this._tooltip !== tooltip) { @@ -314,4 +314,4 @@ class RemoteBadge extends Disposable { updateTitle(); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 7ddf7c7fa7016..31f9d115e24f8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -131,7 +131,7 @@ class Extension implements IExtension { private get localIconUrl(): string | null { if (this.local && this.local.manifest.icon) { - return asDomUri(resources.joinPath(this.local.location, this.local.manifest.icon)).toString(); + return asDomUri(resources.joinPath(this.local.location, this.local.manifest.icon)).toString(true); } return null; } @@ -484,7 +484,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private readonly _onChange: Emitter = new Emitter(); get onChange(): Event { return this._onChange.event; } - private _extensionAllowedBadgeProviders: string[]; private installing: IExtension[] = []; constructor( @@ -1020,13 +1019,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return changed; } - get allowedBadgeProviders(): string[] { - if (!this._extensionAllowedBadgeProviders) { - this._extensionAllowedBadgeProviders = (this.productService.extensionAllowedBadgeProviders || []).map(s => s.toLowerCase()); - } - return this._extensionAllowedBadgeProviders; - } - private _activityCallBack: (() => void) | null = null; private updateActivity(): void { if ((this.localExtensions && this.localExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) @@ -1096,12 +1088,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } - private _ignoredAutoUpdateExtensions: string[]; + private _ignoredAutoUpdateExtensions: string[] | undefined; private get ignoredAutoUpdateExtensions(): string[] { if (!this._ignoredAutoUpdateExtensions) { this._ignoredAutoUpdateExtensions = JSON.parse(this.storageService.get('extensions.ignoredAutoUpdateExtension', StorageScope.GLOBAL, '[]') || '[]'); } - return this._ignoredAutoUpdateExtensions; + return this._ignoredAutoUpdateExtensions!; } private set ignoredAutoUpdateExtensions(extensionIds: string[]) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensions.css b/src/vs/workbench/contrib/extensions/browser/media/extensions.css index 4a74b3ce38dea..ad1858f6f0567 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensions.css @@ -6,8 +6,3 @@ .monaco-workbench .activitybar > .content .monaco-action-bar .action-label.extensions { -webkit-mask: url('extensions-activity-bar.svg') no-repeat 50% 50%; } - -.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/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index ede6bff2a6445..57be7e4e1d49c 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -27,13 +27,16 @@ height: calc(100% - 38px); } -.extensions-viewlet > .extensions .list-actionbar-container .monaco-action-bar .action-item > .octicon { - font-size: 12px; - line-height: 1; - margin-right: 10px; +.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar { + margin-right: 4px; +} + +.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar .action-item > .action-label.icon.octicon { + vertical-align: middle; + line-height: 22px; } -.extensions-viewlet > .extensions .list-actionbar-container .monaco-action-bar .action-item.disabled { +.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar .action-item.disabled { display: none; } @@ -44,7 +47,7 @@ } .extensions-viewlet > .extensions .panel-header { - padding-right: 28px; + padding-right: 6px; } .extensions-viewlet > .extensions .panel-header > .title { diff --git a/src/vs/workbench/contrib/extensions/browser/media/markdown.css b/src/vs/workbench/contrib/extensions/browser/media/markdown.css deleted file mode 100644 index 521ae95b08274..0000000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/markdown.css +++ /dev/null @@ -1,175 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -body { - padding: 10px 20px; - line-height: 22px; -} - -img { - max-width: 100%; - max-height: 100%; -} - -a { - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -a:focus, -input:focus, -select:focus, -textarea:focus { - outline: 1px solid -webkit-focus-ring-color; - outline-offset: -1px; -} - -hr { - border: 0; - height: 2px; - border-bottom: 2px solid; -} - -h1 { - padding-bottom: 0.3em; - line-height: 1.2; - border-bottom-width: 1px; - border-bottom-style: solid; -} - -h1, h2, h3 { - font-weight: normal; -} - -table { - border-collapse: collapse; -} - -table > thead > tr > th { - text-align: left; - border-bottom: 1px solid; -} - -table > thead > tr > th, -table > thead > tr > td, -table > tbody > tr > th, -table > tbody > tr > td { - padding: 5px 10px; -} - -table > tbody > tr + tr > td { - border-top: 1px solid; -} - -blockquote { - margin: 0 7px 0 5px; - padding: 0 16px 0 10px; - border-left-width: 5px; - border-left-style: solid; -} - -code { - font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; - font-size: 14px; - line-height: 19px; -} - -.mac code { - font-size: 12px; - line-height: 18px; -} - -code > div { - padding: 16px; - border-radius: 3px; - overflow: auto; -} - -#scroll-to-top { - position: fixed; - width: 40px; - height: 40px; - right: 25px; - bottom: 25px; - background-color:#444444; - border-radius: 50%; - cursor: pointer; - box-shadow: 1px 1px 1px rgba(0,0,0,.25); - outline: none; - display: flex; - justify-content: center; - align-items: center; -} - -#scroll-to-top:hover { - background-color:#007acc; - box-shadow: 2px 2px 2px rgba(0,0,0,.25); -} - -body.vscode-light #scroll-to-top { - background-color: #949494; -} - -body.vscode-high-contrast #scroll-to-top:hover { - background-color: #007acc; -} - -body.vscode-high-contrast #scroll-to-top { - background-color: black; - border: 2px solid #6fc3df; - box-shadow: none; -} -body.vscode-high-contrast #scroll-to-top:hover { - background-color: #007acc; -} - -#scroll-to-top span.icon::before { - content: ""; - display: block; - /* Chevron up icon */ - background:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo='); - width: 16px; - height: 16px; -} - -/** Theming */ -.vscode-light code > div { - background-color: rgba(220, 220, 220, 0.4); -} - -.vscode-dark code > div { - background-color: rgba(10, 10, 10, 0.4); -} - -.vscode-high-contrast code > div { - background-color: rgb(0, 0, 0); -} - -.vscode-high-contrast h1 { - border-color: rgb(0, 0, 0); -} - -.vscode-light table > thead > tr > th { - border-color: rgba(0, 0, 0, 0.69); -} - -.vscode-dark table > thead > tr > th { - border-color: rgba(255, 255, 255, 0.69); -} - -.vscode-light h1, -.vscode-light hr, -.vscode-light table > tbody > tr + tr > td { - border-color: rgba(0, 0, 0, 0.18); -} - -.vscode-dark h1, -.vscode-dark hr, -.vscode-dark table > tbody > tr + tr > td { - border-color: rgba(255, 255, 255, 0.18); -} diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 33e46eecf825e..47b8e916d88e0 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -91,7 +91,6 @@ export interface IExtensionsWorkbenchService { setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise; open(extension: IExtension, sideByside?: boolean): Promise; checkForUpdates(): Promise; - allowedBadgeProviders: string[]; } export const ConfigurationKey = 'extensions'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 643c6e1b4563b..4c21e1e91815e 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -7,9 +7,7 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; -import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/electron-browser/extensionTipsService'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -25,7 +23,6 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; // Singletons -registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -134,4 +131,4 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { }, group: 'navigation', when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts index 11aacd0695543..e7e9842b027fa 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts @@ -17,6 +17,7 @@ import { join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; abstract class RepoInfo { abstract get base(): string; @@ -117,6 +118,7 @@ class ReportExtensionSlowAction extends Action { readonly repoInfo: RepoInfo, readonly profile: IExtensionHostProfile, @IDialogService private readonly _dialogService: IDialogService, + @IOpenerService private readonly _openerService: IOpenerService ) { super('report.slow', localize('cmd.report', "Report Issue")); } @@ -140,7 +142,7 @@ class ReportExtensionSlowAction extends Action { - VSCode version: \`${pkg.version}\`\n\n${message}`); const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues/new/?body=${body}&title=${title}`; - window.open(url); + this._openerService.open(URI.parse(url)); this._dialogService.show( Severity.Info, @@ -158,6 +160,7 @@ class ShowExtensionSlowAction extends Action { readonly repoInfo: RepoInfo, readonly profile: IExtensionHostProfile, @IDialogService private readonly _dialogService: IDialogService, + @IOpenerService private readonly _openerService: IOpenerService ) { super('show.slow', localize('cmd.show', "Show Issues")); } @@ -172,7 +175,7 @@ class ShowExtensionSlowAction extends Action { // show issues const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues?utf8=✓&q=is%3Aissue+state%3Aopen+%22Extension+causes+high+cpu+load%22`; - window.open(url); + this._openerService.open(URI.parse(url)); this._dialogService.show( Severity.Info, diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 1cc01b85802ea..b6ac536b0898d 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -44,6 +44,8 @@ import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/pl import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -120,7 +122,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IStorageService storageService: IStorageService, @ILabelService private readonly _labelService: ILabelService, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IOpenerService private readonly _openerService: IOpenerService ) { super(RuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); @@ -312,7 +315,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true }); } if (isNonEmptyArray(element.status.runtimeErrors)) { - data.actionbar.push(new ReportExtensionIssueAction(element), { icon: true, label: true }); + data.actionbar.push(new ReportExtensionIssueAction(element, this._openerService), { icon: true, label: true }); } let title: string; @@ -416,7 +419,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { const actions: IAction[] = []; - actions.push(new ReportExtensionIssueAction(e.element)); + actions.push(new ReportExtensionIssueAction(e.element, this._openerService)); actions.push(new Separator()); if (e.element.marketplaceInfo) { @@ -480,7 +483,7 @@ export class ReportExtensionIssueAction extends Action { marketplaceInfo: IExtension; status?: IExtensionsStatus; unresponsiveProfile?: IExtensionHostProfile - }) { + }, @IOpenerService private readonly openerService: IOpenerService) { super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue'); this.enabled = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User @@ -490,7 +493,7 @@ export class ReportExtensionIssueAction extends Action { } async run(): Promise { - window.open(this._url); + this.openerService.open(URI.parse(this._url)); } private static _generateNewIssueUrl(extension: { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index bce6e57e28702..93e10545ebde7 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -16,7 +16,7 @@ import { import { IExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/electron-browser/extensionTipsService'; +import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IURLService } from 'vs/platform/url/common/url'; @@ -31,7 +31,7 @@ import { TestContextService, TestWindowService, TestSharedProcessService } from import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IWindowService } from 'vs/platform/windows/common/windows'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts index cd6d14c858d56..1ac40b499a25a 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -15,14 +15,14 @@ import { DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/electron-browser/extensionTipsService'; +import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService, TestLifecycleService, TestSharedProcessService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestLifecycleService, TestSharedProcessService, productService } from 'vs/workbench/test/workbenchTestServices'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; @@ -36,12 +36,11 @@ import { ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensi import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test'; import { IURLService } from 'vs/platform/url/common/url'; -import product from 'vs/platform/product/node/product'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService, Severity, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -52,6 +51,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; +import { IProductService } from 'vs/platform/product/common/product'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -201,27 +201,31 @@ suite('ExtensionsTipsService Test', () => { instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IURLService, URLService); + instantiationService.set(IProductService, { + ...productService, + ...{ + extensionTips: { + 'ms-vscode.csharp': '{**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.sln,**/appsettings.json}', + 'msjsdiag.debugger-for-chrome': '{**/*.ts,**/*.tsx**/*.js,**/*.jsx,**/*.es6,**/.babelrc}', + 'lukehoban.Go': '**/*.go' + }, + extensionImportantTips: { + 'ms-python.python': { + 'name': 'Python', + 'pattern': '{**/*.py}' + }, + 'ms-vscode.PowerShell': { + 'name': 'PowerShell', + 'pattern': '{**/*.ps,**/*.ps1}' + } + } + } + }); experimentService = instantiationService.createInstance(TestExperimentService); instantiationService.stub(IExperimentService, experimentService); onModelAddedEvent = new Emitter(); - - product.extensionTips = { - 'ms-vscode.csharp': '{**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.sln,**/appsettings.json}', - 'msjsdiag.debugger-for-chrome': '{**/*.ts,**/*.tsx**/*.js,**/*.jsx,**/*.es6,**/.babelrc}', - 'lukehoban.Go': '**/*.go' - }; - product.extensionImportantTips = { - 'ms-python.python': { - 'name': 'Python', - 'pattern': '{**/*.py}' - }, - 'ms-vscode.PowerShell': { - 'name': 'PowerShell', - 'pattern': '{**/*.ps,**/*.ps1}' - } - }; }); suiteTeardown(() => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index cf2b6c795226f..50dac5ecc8b2f 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -17,7 +17,7 @@ import { import { IExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionTipsService, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/electron-browser/extensionTipsService'; +import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IURLService } from 'vs/platform/url/common/url'; @@ -31,12 +31,11 @@ import { TestContextService, TestWindowService, TestSharedProcessService } from import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IWindowService } from 'vs/platform/windows/common/windows'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { SinonStub } from 'sinon'; -import { IExperimentService, ExperimentState, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { ExperimentService } from 'vs/workbench/contrib/experiments/electron-browser/experimentService'; +import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 04ed61d0d8a7c..f6ca00e7b8897 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -17,7 +17,7 @@ import { import { IExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/electron-browser/extensionTipsService'; +import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IURLService } from 'vs/platform/url/common/url'; @@ -34,7 +34,7 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { ProgressService } from 'vs/workbench/services/progress/browser/progressService'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index 618dc7edaa9d5..ef308647b8df1 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -20,6 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IProductService } from 'vs/platform/product/common/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export interface IFeedback { feedback: string; @@ -27,7 +28,7 @@ export interface IFeedback { } export interface IFeedbackDelegate { - submitFeedback(feedback: IFeedback): void; + submitFeedback(feedback: IFeedback, openerService: IOpenerService): void; getCharacterLimit(sentiment: number): number; } @@ -66,7 +67,8 @@ export class FeedbackDropdown extends Dropdown { @IIntegrityService private readonly integrityService: IIntegrityService, @IThemeService private readonly themeService: IThemeService, @IStatusbarService private readonly statusbarService: IStatusbarService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IOpenerService private readonly openerService: IOpenerService ) { super(container, options); @@ -415,7 +417,7 @@ export class FeedbackDropdown extends Dropdown { this.feedbackDelegate.submitFeedback({ feedback: this.feedbackDescriptionInput.value, sentiment: this.sentiment - }); + }, this.openerService); this.hide(); } diff --git a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index c609aa4132fa3..9eda590df6fcb 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -12,6 +12,8 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarService, StatusbarAlignment, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; import { localize } from 'vs/nls'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; class TwitterFeedbackService implements IFeedbackDelegate { @@ -23,11 +25,11 @@ class TwitterFeedbackService implements IFeedbackDelegate { return TwitterFeedbackService.HASHTAGS.join(','); } - submitFeedback(feedback: IFeedback): void { + submitFeedback(feedback: IFeedback, openerService: IOpenerService): void { const queryString = `?${feedback.sentiment === 1 ? `hashtags=${this.combineHashTagsAsString()}&` : null}ref_src=twsrc%5Etfw&related=twitterapi%2Ctwitter&text=${encodeURIComponent(feedback.feedback)}&tw_p=tweetbutton&via=${TwitterFeedbackService.VIA_NAME}`; const url = TwitterFeedbackService.TWITTER_URL + queryString; - window.open(url); + openerService.open(URI.parse(url)); } getCharacterLimit(sentiment: number): number { diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 580a2990ac4f9..9c0808d9ad1ae 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -7,16 +7,15 @@ import * as nls from 'vs/nls'; import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { URI } from 'vs/base/common/uri'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; /** * An implementation of editor for binary files like images. @@ -28,7 +27,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IWindowsService private readonly windowsService: IWindowsService, + @IOpenerService private readonly openerService: IOpenerService, @IEditorService private readonly editorService: IEditorService, @IStorageService storageService: IStorageService, @IFileService fileService: IFileService, @@ -39,7 +38,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { BinaryFileEditor.ID, { openInternal: (input, options) => this.openInternal(input, options), - openExternal: resource => this.openExternal(resource) + openExternal: resource => this.openerService.open(resource, { openExternal: true }) }, telemetryService, themeService, @@ -58,13 +57,6 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } } - private async openExternal(resource: URI): Promise { - const didOpen = await this.windowsService.openExternal(resource.toString()); - if (!didOpen) { - return this.windowsService.showItemInFolder(resource); - } - } - getTitle(): string | null { return this.input ? this.input.getName() : nls.localize('binaryFileEditor', "Binary File Viewer"); } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index c3e3a622b58d1..c9713c80989ea 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -34,6 +34,7 @@ import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { withUndefinedAsNull } from 'vs/base/common/types'; export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -202,7 +203,7 @@ export class ExplorerViewlet extends ViewContainerViewlet { openEditorsView.setStructuralRefreshDelay(delay); } - let openedEditor: IEditor | null = null; + let openedEditor: IEditor | undefined; try { openedEditor = await this.editorService.openEditor(editor, options, group); } catch (error) { @@ -214,7 +215,7 @@ export class ExplorerViewlet extends ViewContainerViewlet { } } - return openedEditor; + return withUndefinedAsNull(openedEditor); }); const explorerInstantiator = this.instantiationService.createChild(new ServiceCollection([IEditorService, delegatingEditorService])); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 8a2c0fe652f85..e204c90c0bcd6 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -44,7 +44,7 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -471,7 +471,7 @@ export class GlobalCompareResourcesAction extends Action { override: this.editorService.openEditor({ leftResource: activeResource, rightResource: resource - }).then(() => null) + }).then(() => undefined) }; } @@ -1108,7 +1108,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { return await fileService.copy(fileToPaste, targetFile); } } catch (e) { - onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile"))); + onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile. {0}", getErrorMessage(e)))); return undefined; } })); diff --git a/src/vs/workbench/contrib/files/browser/media/check-dark.svg b/src/vs/workbench/contrib/files/browser/media/check-dark.svg index 865cc83c347af..51674695e1fde 100644 --- a/src/vs/workbench/contrib/files/browser/media/check-dark.svg +++ b/src/vs/workbench/contrib/files/browser/media/check-dark.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/files/browser/media/check-light.svg b/src/vs/workbench/contrib/files/browser/media/check-light.svg index e1a546660ed15..7b1da6d720863 100644 --- a/src/vs/workbench/contrib/files/browser/media/check-light.svg +++ b/src/vs/workbench/contrib/files/browser/media/check-light.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index ac09186d04619..608638b517820 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -236,7 +236,6 @@ class ResolveSaveConflictAction extends Action { @IEditorService private readonly editorService: IEditorService, @INotificationService private readonly notificationService: INotificationService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStorageService private readonly storageService: IStorageService, @IEnvironmentService private readonly environmentService: IEnvironmentService ) { super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare")); @@ -250,21 +249,15 @@ class ResolveSaveConflictAction extends Action { await TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }); - if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) { - return; // return if this message is ignored - } - // Show additional help how to resolve the save conflict - const primaryActions: IAction[] = [ - this.instantiationService.createInstance(ResolveConflictLearnMoreAction) - ]; - const secondaryActions: IAction[] = [ - this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction) - ]; - - const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions }; - const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions }); - Event.once(handle.onDidClose)(() => { dispose(primaryActions); dispose(secondaryActions); }); + const actions: INotificationActions = { primary: [this.instantiationService.createInstance(ResolveConflictLearnMoreAction)] }; + const handle = this.notificationService.notify({ + severity: Severity.Info, + message: conflictEditorHelp, + actions, + neverShowAgain: { id: LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, isSecondary: true } + }); + Event.once(handle.onDidClose)(() => dispose(actions.primary!)); pendingResolveSaveConflictMessages.push(handle); } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 8f097112f0072..e7e9ecf5f6155 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -59,7 +59,6 @@ export class EmptyView extends ViewletPanel { container.appendChild(titleContainer); this.titleElement = document.createElement('span'); - this.titleElement.textContent = name; titleContainer.appendChild(this.titleElement); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts index f1708ff4d4969..eb1f7705faf9f 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts @@ -11,6 +11,7 @@ import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/dec import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { explorerRootErrorEmitter } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; export class ExplorerDecorationsProvider implements IDecorationsProvider { readonly label: string = localize('label', "Explorer"); @@ -30,6 +31,9 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { this._onDidChange.fire([change.item.resource]); } })); + this.toDispose.add(explorerRootErrorEmitter.event((resource => { + this._onDidChange.fire([resource]); + }))); } get onDidChange(): Event { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index c5bd34e7ac6a8..5a2c00e57860c 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -50,6 +50,7 @@ import { first } from 'vs/base/common/arrays'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { dispose } from 'vs/base/common/lifecycle'; +import { timeout } from 'vs/base/common/async'; export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; @@ -522,6 +523,8 @@ export class ExplorerView extends ViewletPanel { while (item && item.resource.toString() !== resource.toString()) { await this.tree.expand(item); + // Tree returns too early from the expand, need to wait for next tick #77106 + await timeout(0); item = first(values(item.children), i => isEqualOrParent(resource, i.resource)); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 75aae73d1448b..0c368120dff74 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -46,6 +46,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { Emitter } from 'vs/base/common/event'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -60,6 +61,7 @@ export class ExplorerDelegate implements IListVirtualDelegate { } } +export const explorerRootErrorEmitter = new Emitter(); export class ExplorerDataSource implements IAsyncDataSource { constructor( @@ -87,8 +89,9 @@ export class ExplorerDataSource implements IAsyncDataSource { - this.updateWidth(stat); + try { + this.updateWidth(stat); + } catch (e) { + // noop since the element might no longer be in the tree, no update of width necessery + } }); } @@ -218,56 +225,46 @@ export class FilesRenderer implements ITreeRenderer 0 && !stat.isDirectory ? lastDot : value.length }); - let isFinishableDisposeEvent = false; - setTimeout(() => { - // Check if disposed - if (!inputBox.inputElement) { - return; - } - inputBox.focus(); - inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length }); - isFinishableDisposeEvent = true; - }, 0); - - const done = once(async (success: boolean) => { + const done = once(async (success: boolean, blur: boolean) => { label.element.style.display = 'none'; const value = inputBox.value; dispose(toDispose); - label.element.remove(); - // Timeout: once done rendering only then re-render #70902 - setTimeout(() => editableData.onFinish(value, success), 0); + container.removeChild(label.element); + editableData.onFinish(value, success); }); - const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { - done(inputBox.isInputValid()); - }); + // It can happen that the tree re-renders this node. When that happens, + // we're gonna get a blur event first and only after an element disposable. + // Because of that, we should setTimeout the blur handler to differentiate + // between the blur happening because of a unrender or because of a user action. + let ignoreBlur = false; const toDispose = [ inputBox, DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => { if (e.equals(KeyCode.Enter)) { if (inputBox.validate()) { - done(true); + done(true, false); } } else if (e.equals(KeyCode.Escape)) { - done(false); + done(false, false); } }), - blurDisposable, + DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { + setTimeout(() => { + if (!ignoreBlur) { + done(inputBox.isInputValid(), true); + } + }, 0); + }), label, styler ]; - return toDisposable(() => { - if (isFinishableDisposeEvent) { - done(false); - } - else { - dispose(toDispose); - label.element.remove(); - } - }); + return toDisposable(() => ignoreBlur = true); } disposeElement?(element: ITreeNode, index: number, templateData: IFileTemplateData): void { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 96be5e2659b93..1aa98445d91be 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -28,7 +28,7 @@ import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAn import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; @@ -263,15 +263,21 @@ export class OpenEditorsView extends ViewletPanel { let openToSide = false; let isSingleClick = false; let isDoubleClick = false; + let isMiddleClick = false; if (browserEvent instanceof MouseEvent) { isSingleClick = browserEvent.detail === 1; isDoubleClick = browserEvent.detail === 2; + isMiddleClick = browserEvent.button === 1; openToSide = this.list.useAltAsMultipleSelectionModifier ? (browserEvent.ctrlKey || browserEvent.metaKey) : browserEvent.altKey; } const focused = this.list.getFocusedElements(); const element = focused.length ? focused[0] : undefined; if (element instanceof OpenEditor) { + if (isMiddleClick) { + return; // already handled above: closes the editor + } + this.openEditor(element, { preserveFocus: isSingleClick, pinned: isDoubleClick, sideBySide: openToSide }); } else if (element) { this.editorGroupService.activateGroup(element); @@ -349,9 +355,9 @@ export class OpenEditorsView extends ViewletPanel { const preserveActivateGroup = options.sideBySide && options.preserveFocus; // needed for https://github.com/Microsoft/vscode/issues/42399 if (!preserveActivateGroup) { - this.editorGroupService.activateGroup(element.groupId); // needed for https://github.com/Microsoft/vscode/issues/6672 + this.editorGroupService.activateGroup(element.group); // needed for https://github.com/Microsoft/vscode/issues/6672 } - this.editorService.openEditor(element.editor, options, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { + this.editorService.openEditor(element.editor, options, options.sideBySide ? SIDE_GROUP : element.group).then(editor => { if (editor && !preserveActivateGroup && editor.group) { this.editorGroupService.activateGroup(editor.group); } diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 2d25923a2c273..b95d1949bdf58 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { memoize } from 'vs/base/common/decorators'; -import { basename } from 'vs/base/common/path'; import { dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor'; @@ -147,14 +146,14 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { getName(): string { if (!this.name) { - this.name = basename(this.labelService.getUriLabel(this.resource)); + this.name = this.labelService.getUriBasenameLabel(this.resource); } return this.decorateLabel(this.name); } private get shortDescription(): string { - return basename(this.labelService.getUriLabel(dirname(this.resource))); + return this.labelService.getUriBasenameLabel(dirname(this.resource)); } private get mediumDescription(): string { @@ -203,17 +202,20 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { switch (verbosity) { case Verbosity.SHORT: title = this.shortTitle; + // already decorated by getName() break; default: case Verbosity.MEDIUM: title = this.mediumTitle; + title = this.decorateLabel(title); break; case Verbosity.LONG: title = this.longTitle; + title = this.decorateLabel(title); break; } - return this.decorateLabel(title); + return title; } private decorateLabel(label: string): string { diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index aefd67e415d15..6bba996f279f6 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -148,7 +148,7 @@ export class ExplorerItem { return this === this.root; } - static create(raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: URI[]): ExplorerItem { + static create(raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { const stat = new ExplorerItem(raw.resource, parent, raw.isDirectory, raw.isSymbolicLink, raw.isReadonly, raw.name, raw.mtime); // Recursively add children if present @@ -247,9 +247,14 @@ export class ExplorerItem { // Resolve metadata only when the mtime is needed since this can be expensive // Mtime is only used when the sort order is 'modified' const resolveMetadata = explorerService.sortOrder === 'modified'; - const stat = await fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata }); - const resolved = ExplorerItem.create(stat, this); - ExplorerItem.mergeLocalWithDisk(resolved, this); + try { + const stat = await fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata }); + const resolved = ExplorerItem.create(stat, this); + ExplorerItem.mergeLocalWithDisk(resolved, this); + } catch (e) { + this.isError = true; + throw e; + } this._isDirectoryResolved = true; } diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index 9d0f177a77a94..aff4390570c49 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -158,7 +158,11 @@ export class ExplorerService implements IExplorerService { // Stat needs to be resolved first and then revealed const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === 'modified' }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); - const rootUri = workspaceFolder ? workspaceFolder.uri : this.roots[0].resource; + if (workspaceFolder === null) { + return Promise.resolve(undefined); + } + const rootUri = workspaceFolder.uri; + const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop()!; try { diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts index 3aa862c759987..1cb252b5e686c 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts @@ -13,7 +13,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { WorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issueService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IIssueService } from 'vs/platform/issue/common/issue'; +import { IIssueService } from 'vs/platform/issue/node/issue'; const helpCategory = { value: nls.localize('help', "Help"), original: 'Help' }; const workbenchActionsRegistry = Registry.as(Extensions.WorkbenchActions); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.ts index 2cc057f6bccd4..ca98eb60acae1 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IssueReporterData } from 'vs/platform/issue/common/issue'; +import { IssueReporterData } from 'vs/platform/issue/node/issue'; export const IWorkbenchIssueService = createDecorator('workbenchIssueService'); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts b/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts index 4eaaf7c30296b..4b6b25c31c0a4 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts @@ -5,7 +5,7 @@ import { Action } from 'vs/base/common/actions'; import * as nls from 'vs/nls'; -import { IssueType } from 'vs/platform/issue/common/issue'; +import { IssueType } from 'vs/platform/issue/node/issue'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; export class OpenProcessExplorer extends Action { diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts index e93eac14556a1..5dfea6f09da4f 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IssueReporterStyles, IIssueService, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; +import { IssueReporterStyles, IIssueService, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/node/issue'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts index 0ab7785ff311e..1982d8aebc025 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts @@ -68,8 +68,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo } private onDidInstallExtension(e: DidInstallExtensionEvent): void { - const donotAskUpdateKey = 'langugage.update.donotask'; - if (!this.storageService.getBoolean(donotAskUpdateKey, StorageScope.GLOBAL) && e.local && e.operation === InstallOperation.Install && e.local.manifest.contributes && e.local.manifest.contributes.localizations && e.local.manifest.contributes.localizations.length) { + if (e.local && e.operation === InstallOperation.Install && e.local.manifest.contributes && e.local.manifest.contributes.localizations && e.local.manifest.contributes.localizations.length) { const locale = e.local.manifest.contributes.localizations[0].languageId; if (platform.language !== locale) { const updateAndRestart = platform.locale !== locale; @@ -83,12 +82,11 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo const updatePromise = updateAndRestart ? this.jsonEditingService.write(this.environmentService.localeResource, { key: 'locale', value: locale }, true) : Promise.resolve(undefined); updatePromise.then(() => this.windowsService.relaunch({}), e => this.notificationService.error(e)); } - }, { - label: localize('neverAgain', "Don't Show Again"), - isSecondary: true, - run: () => this.storageService.store(donotAskUpdateKey, true, StorageScope.GLOBAL) }], - { sticky: true } + { + sticky: true, + neverShowAgain: { id: 'langugage.update.donotask', isSecondary: true } + } ); } } @@ -302,4 +300,4 @@ ExtensionsRegistry.registerExtensionPoint({ } } } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index fa261bc537bd6..b2ad741a15ea7 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -4,11 +4,90 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import { join } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { SetLogLevelAction, OpenLogsFolderAction, OpenSessionLogFileAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/contrib/output/common/output'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { dirname } from 'vs/base/common/resources'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.LABEL), 'Developer: Set Log Level...', devCategory); \ No newline at end of file +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.LABEL), 'Developer: Set Log Level...', devCategory); + +class LogOutputChannels extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + if (isWeb) { + this.registerWebContributions(); + } else { + this.registerNativeContributions(); + } + } + + private registerWebContributions(): void { + Registry.as(OutputExt.OutputChannels).registerChannel({ id: Constants.rendererLogChannelId, label: nls.localize('rendererLog', "Window"), file: this.environmentService.logFile, log: true }); + this.instantiationService.createInstance(LogsDataCleaner); + + const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); + const devCategory = nls.localize('developer', "Developer"); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenSessionLogFileAction, OpenSessionLogFileAction.ID, OpenSessionLogFileAction.LABEL), 'Developer: Open Log File (Session)...', devCategory); + } + + private registerNativeContributions(): void { + this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(this.environmentService.logsPath, `main.log`))); + this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); + this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), this.environmentService.logFile); + + const registerTelemetryChannel = (level: LogLevel) => { + if (level === LogLevel.Trace && !Registry.as(OutputExt.OutputChannels).getChannel(Constants.telemetryLogChannelId)) { + this.registerLogChannel(Constants.telemetryLogChannelId, nls.localize('telemetryLog', "Telemetry"), URI.file(join(this.environmentService.logsPath, `telemetry.log`))); + } + }; + registerTelemetryChannel(this.logService.getLevel()); + this.logService.onDidChangeLogLevel(registerTelemetryChannel); + + const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); + const devCategory = nls.localize('developer', "Developer"); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLogsFolderAction, OpenLogsFolderAction.ID, OpenLogsFolderAction.LABEL), 'Developer: Open Logs Folder', devCategory); + } + + private async registerLogChannel(id: string, label: string, file: URI): Promise { + const outputChannelRegistry = Registry.as(OutputExt.OutputChannels); + const exists = await this.fileService.exists(file); + if (exists) { + outputChannelRegistry.registerChannel({ id, label, file, log: true }); + return; + } + + const watcher = this.fileService.watch(dirname(file)); + const disposable = this.fileService.onFileChanges(e => { + if (e.contains(file, FileChangeType.ADDED)) { + watcher.dispose(); + disposable.dispose(); + outputChannelRegistry.registerChannel({ id, label, file, log: true }); + } + }); + } + +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 56b54a4c89cb7..7bf78df5a3b0b 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -9,8 +9,12 @@ import { join } from 'vs/base/common/path'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { ILogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { dirname, basename, isEqual } from 'vs/base/common/resources'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class OpenLogsFolderAction extends Action { @@ -73,3 +77,68 @@ export class SetLogLevelAction extends Action { return undefined; } } + +export class OpenSessionLogFileAction extends Action { + + static ID = 'workbench.action.openSessionLogFile'; + static LABEL = nls.localize('openSessionLogFile', "Open Log File (Session)..."); + + constructor(id: string, label: string, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IEditorService private readonly editorService: IEditorService, + ) { + super(id, label); + } + + async run(): Promise { + const sessionResult = await this.quickInputService.pick( + this.getSessions().then(sessions => sessions.map((s, index) => ({ + id: s.toString(), + label: basename(s), + description: index === 0 ? nls.localize('current', "Current") : undefined + }))), + { + canPickMany: false, + placeHolder: nls.localize('sessions placeholder', "Select Session") + }); + if (sessionResult) { + const logFileResult = await this.quickInputService.pick( + this.getLogFiles(URI.parse(sessionResult.id!)).then(logFiles => logFiles.map(s => ({ + id: s.toString(), + label: basename(s) + }))), + { + canPickMany: false, + placeHolder: nls.localize('log placeholder', "Select Log file") + }); + if (logFileResult) { + return this.editorService.openEditor({ resource: URI.parse(logFileResult.id!) }).then(() => undefined); + } + } + } + + private async getSessions(): Promise { + const logsPath = URI.file(this.environmentService.logsPath).with({ scheme: this.environmentService.logFile.scheme }); + const result: URI[] = [logsPath]; + const stat = await this.fileService.resolve(dirname(logsPath)); + if (stat.children) { + result.push(...stat.children + .filter(stat => !isEqual(stat.resource, logsPath) && stat.isDirectory && /^\d{8}T\d{6}$/.test(stat.name)) + .sort() + .reverse() + .map(d => d.resource)); + } + return result; + } + + private async getLogFiles(session: URI): Promise { + const stat = await this.fileService.resolve(session); + if (stat.children) { + return stat.children.filter(stat => !stat.isDirectory).map(stat => stat.resource); + } + return []; + } +} + diff --git a/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts new file mode 100644 index 0000000000000..4f7faa55ebe6e --- /dev/null +++ b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { basename, dirname } from 'vs/base/common/resources'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { URI } from 'vs/base/common/uri'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; + +export class LogsDataCleaner extends Disposable { + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileService private readonly fileService: IFileService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + ) { + super(); + this.cleanUpOldLogsSoon(); + } + + private cleanUpOldLogsSoon(): void { + let handle: any = setTimeout(async () => { + handle = undefined; + const logsPath = URI.file(this.environmentService.logsPath).with({ scheme: this.environmentService.logFile.scheme }); + const stat = await this.fileService.resolve(dirname(logsPath)); + if (stat.children) { + const currentLog = basename(logsPath); + const allSessions = stat.children.filter(stat => stat.isDirectory && /^\d{8}T\d{6}$/.test(stat.name)); + const oldSessions = allSessions.sort().filter((d, i) => d.name !== currentLog); + const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 49)); + Promise.all(toDelete.map(stat => this.fileService.del(stat.resource, { recursive: true }))); + } + }, 10 * 1000); + this.lifecycleService.onWillShutdown(() => { + if (handle) { + clearTimeout(handle); + handle = undefined; + } + }); + } +} diff --git a/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts b/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts deleted file mode 100644 index d8d381fa5252b..0000000000000 --- a/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts +++ /dev/null @@ -1,68 +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 nls from 'vs/nls'; -import { join } from 'vs/base/common/path'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/contrib/output/common/output'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { OpenLogsFolderAction } from 'vs/workbench/contrib/logs/common/logsActions'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; - -class LogOutputChannels extends Disposable implements IWorkbenchContribution { - - constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @ILogService logService: ILogService, - @IFileService private readonly fileService: IFileService - ) { - super(); - this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(environmentService.logsPath, `main.log`))); - this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(environmentService.logsPath, `sharedprocess.log`))); - this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), URI.file(join(environmentService.logsPath, `renderer${environmentService.configuration.windowId}.log`))); - - const registerTelemetryChannel = (level: LogLevel) => { - if (level === LogLevel.Trace && !Registry.as(OutputExt.OutputChannels).getChannel(Constants.telemetryLogChannelId)) { - this.registerLogChannel(Constants.telemetryLogChannelId, nls.localize('telemetryLog', "Telemetry"), URI.file(join(environmentService.logsPath, `telemetry.log`))); - } - }; - registerTelemetryChannel(logService.getLevel()); - logService.onDidChangeLogLevel(registerTelemetryChannel); - - const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); - const devCategory = nls.localize('developer', "Developer"); - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLogsFolderAction, OpenLogsFolderAction.ID, OpenLogsFolderAction.LABEL), 'Developer: Open Logs Folder', devCategory); - } - - private async registerLogChannel(id: string, label: string, file: URI): Promise { - const outputChannelRegistry = Registry.as(OutputExt.OutputChannels); - const exists = await this.fileService.exists(file); - if (exists) { - outputChannelRegistry.registerChannel({ id, label, file, log: true }); - return; - } - - const watcher = this.fileService.watch(dirname(file)); - const disposable = this.fileService.onFileChanges(e => { - if (e.contains(file, FileChangeType.ADDED)) { - watcher.dispose(); - disposable.dispose(); - outputChannelRegistry.registerChannel({ id, label, file, log: true }); - } - }); - } - -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Restored); \ No newline at end of file diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersPanel.ts index 71b13da14132f..36f1b9bd36576 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanel.ts @@ -534,8 +534,10 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { const span1 = dom.append(container, dom.$('span')); span1.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS; const link = dom.append(container, dom.$('a.messageAction')); - link.textContent = localize('clearFilter', "Clear Filter."); + link.textContent = localize('clearFilter', "Clear Filter"); link.setAttribute('tabIndex', '0'); + const span2 = dom.append(container, dom.$('span')); + span2.textContent = '.'; dom.addStandardDisposableListener(link, dom.EventType.CLICK, () => this.filterAction.filterText = ''); dom.addStandardDisposableListener(link, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { diff --git a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts index 001525543496f..90dfc9732bf68 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts @@ -122,7 +122,8 @@ class RequestOracle { let handle: any; let contentListener = codeEditor.onDidChangeModelContent(event => { clearTimeout(handle); - handle = setTimeout(() => this._callback(codeEditor!, event), 350); + const timeout = OutlineModel.getRequestDelay(codeEditor!.getModel()); + handle = setTimeout(() => this._callback(codeEditor!, event), timeout); }); let modeListener = codeEditor.onDidChangeModelLanguage(_ => { this._callback(codeEditor!, undefined); diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 13d736f2db0a3..6e7141a041b7f 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -14,14 +14,11 @@ import { EditorOptions } from 'vs/workbench/common/editor'; import { IOutputChannelDescriptor, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, LOG_SCHEME, CONTEXT_ACTIVE_LOG_OUTPUT, LOG_MIME, OUTPUT_MIME } from 'vs/workbench/contrib/output/common/output'; import { OutputPanel } from 'vs/workbench/contrib/output/browser/outputPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/common/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; import { IPanel } from 'vs/workbench/common/panel'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -77,10 +74,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo @IStorageService private readonly storageService: IStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPanelService private readonly panelService: IPanelService, - @IWorkspaceContextService contextService: IWorkspaceContextService, @ITextModelService textModelResolverService: ITextModelService, - @IEnvironmentService environmentService: IEnvironmentService, - @IWindowService windowService: IWindowService, @ILogService private readonly logService: ILogService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IContextKeyService private readonly contextKeyService: IContextKeyService, diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts index ecfdd8e2ea0cf..9c8fe678aede6 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts @@ -17,6 +17,7 @@ import { PerfviewInput } from 'vs/workbench/contrib/performance/electron-browser import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { URI } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class StartupProfiler implements IWorkbenchContribution { @@ -28,6 +29,7 @@ export class StartupProfiler implements IWorkbenchContribution { @IClipboardService private readonly _clipboardService: IClipboardService, @ILifecycleService lifecycleService: ILifecycleService, @IExtensionService extensionService: IExtensionService, + @IOpenerService private readonly _openerService: IOpenerService ) { // wait for everything to be ready Promise.all([ @@ -116,6 +118,6 @@ export class StartupProfiler implements IWorkbenchContribution { const baseUrl = product.reportIssueUrl; const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&'; - window.open(`${baseUrl}${queryStringPrefix}body=${encodeURIComponent(body)}`); + this._openerService.open(URI.parse(`${baseUrl}${queryStringPrefix}body=${encodeURIComponent(body)}`)); } } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index d83000eaaa41a..eb7ff5278a611 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -109,7 +109,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor ) { super(KeybindingsEditor.ID, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); - this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(true))); + this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get()))); this.keybindingsEditorContextKey = CONTEXT_KEYBINDINGS_EDITOR.bindTo(this.contextKeyService); this.searchFocusContextKey = CONTEXT_KEYBINDINGS_SEARCH_FOCUS.bindTo(this.contextKeyService); @@ -537,7 +537,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor } this.unAssignedKeybindingItemToRevealAndFocus = null; } else if (currentSelectedIndex !== -1 && currentSelectedIndex < this.listEntries.length) { - this.selectEntry(currentSelectedIndex); + this.selectEntry(currentSelectedIndex, preserveFocus); } else if (this.editorService.activeControl === this && !preserveFocus) { this.focus(); } @@ -597,11 +597,13 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor return -1; } - private selectEntry(keybindingItemEntry: IKeybindingItemEntry | number): void { + private selectEntry(keybindingItemEntry: IKeybindingItemEntry | number, focus: boolean = true): void { const index = typeof keybindingItemEntry === 'number' ? keybindingItemEntry : this.getIndexOf(keybindingItemEntry); if (index !== -1) { - this.keybindingsList.getHTMLElement().focus(); - this.keybindingsList.setFocus([index]); + if (focus) { + this.keybindingsList.getHTMLElement().focus(); + this.keybindingsList.setFocus([index]); + } this.keybindingsList.setSelection([index]); } } @@ -789,7 +791,7 @@ class KeybindingItemRenderer implements IListRenderer { return this.fileService.createFile(file, VSBuffer.fromString(KeyboardLayoutPickerAction.DEFAULT_CONTENT)); - }).then((stat): Promise | null => { + }).then((stat): Promise | undefined => { if (!stat) { - return null; + return undefined; } return this.editorService.openEditor({ resource: stat.resource, diff --git a/src/vs/workbench/contrib/preferences/browser/media/check-dark.svg b/src/vs/workbench/contrib/preferences/browser/media/check-dark.svg index 865cc83c347af..51674695e1fde 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/check-dark.svg +++ b/src/vs/workbench/contrib/preferences/browser/media/check-dark.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/preferences/browser/media/check-light.svg b/src/vs/workbench/contrib/preferences/browser/media/check-light.svg index e1a546660ed15..7b1da6d720863 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/check-light.svg +++ b/src/vs/workbench/contrib/preferences/browser/media/check-light.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 43d1569f30577..faf363f6a3eaa 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -143,7 +143,7 @@ max-width: 952px; /* 1000 - 24*2 padding */ margin-left: -476px; - z-index: 1000; + z-index: 11; } .settings-editor > .settings-body .settings-tree-container .setting-toolbar-container { @@ -182,7 +182,7 @@ .settings-editor > .settings-body .settings-toc-container { width: 100%; pointer-events: none; - z-index: 100; + z-index: 10; position: absolute; } @@ -369,6 +369,10 @@ margin-top: -1px; z-index: 1; } +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-item-contents.invalid-input .setting-item-validation-message { + position: static; + margin-top: 1rem; +} .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-text .setting-item-validation-message { width: 500px; @@ -511,4 +515,4 @@ .settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count { display: block; -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 34d9362a283ec..b93c9b2fd62d2 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -225,7 +225,7 @@ export class OpenFolderSettingsAction extends Action { return this.preferencesService.openFolderSettings(workspaceFolder.uri); } - return null; + return undefined; }); } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 2887abfc0f3b1..37de5e7766e9f 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -25,6 +25,7 @@ import { nullRange } from 'vs/workbench/services/preferences/common/preferencesM import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStringDictionary } from 'vs/base/common/collections'; import { IProductService } from 'vs/platform/product/common/product'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export interface IEndpointDetails { urlBase?: string; @@ -563,4 +564,6 @@ export class SettingMatches { endColumn: setting.valueRange.startColumn + match.end + 1 }; } -} \ No newline at end of file +} + +registerSingleton(IPreferencesSearchService, PreferencesSearchService, true); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 7eeb9c15120f5..66678cbfffb4c 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -36,7 +36,7 @@ import { ISettingsGroup } from 'vs/workbench/services/preferences/common/prefere export class SettingsHeaderWidget extends Widget implements IViewZone { - private id: number; + private id: string; private _domNode: HTMLElement; protected titleContainer: HTMLElement; @@ -121,7 +121,7 @@ export class DefaultSettingsHeaderWidget extends SettingsHeaderWidget { export class SettingsGroupTitleWidget extends Widget implements IViewZone { - private id: number; + private id: string; private _afterLineNumber: number; private _domNode: HTMLElement; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index ab5ec782a3958..1382fd0df3851 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -458,12 +458,12 @@ export class SettingsEditor2 extends BaseEditor { } } - switchToSettingsFile(): Promise { + switchToSettingsFile(): Promise { const query = parseQuery(this.searchWidget.getValue()); return this.openSettingsFile(query.query); } - private openSettingsFile(query?: string): Promise { + private openSettingsFile(query?: string): Promise { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; const options: ISettingsEditorOptions = { query }; @@ -846,7 +846,7 @@ export class SettingsEditor2 extends BaseEditor { this._register(model.onDidChangeGroups(() => this.onConfigUpdate())); this.defaultSettingsEditorModel = model; - return this.onConfigUpdate(); + return this.onConfigUpdate(undefined, true); }); } return Promise.resolve(null); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 7106918e292a0..0964f8ef312ba 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -224,8 +224,9 @@ interface ISettingComplexItemTemplate extends ISettingItemTemplate { button: Button; } -interface ISettingListItemTemplate extends ISettingItemTemplate { +interface ISettingListItemTemplate extends ISettingItemTemplate { listWidget: ListSettingWidget; + validationErrorMessageElement: HTMLElement; } interface ISettingExcludeItemTemplate extends ISettingItemTemplate { @@ -679,6 +680,9 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr renderTemplate(container: HTMLElement): ISettingListItemTemplate { const common = this.renderCommonTemplate(null, container, 'list'); + const descriptionElement = common.containerElement.querySelector('.setting-item-description')!; + const validationErrorMessageElement = $('.setting-item-validation-message'); + descriptionElement.after(validationErrorMessageElement); const listWidget = this._instantiationService.createInstance(ListSettingWidget, common.controlElement); listWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); @@ -686,19 +690,40 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr const template: ISettingListItemTemplate = { ...common, - listWidget + listWidget, + validationErrorMessageElement }; this.addSettingElementFocusHandler(template); - common.toDispose.push(listWidget.onDidChangeList(e => this.onDidChangeList(template, e))); + common.toDispose.push( + listWidget.onDidChangeList(e => { + const newList = this.computeNewList(template, e); + this.onDidChangeList(template, newList); + if (newList !== null && template.onChange) { + template.onChange(newList); + } + }) + ); return template; } - private onDidChangeList(template: ISettingListItemTemplate, e: IListChangeEvent): void { + private onDidChangeList(template: ISettingListItemTemplate, newList: string[] | undefined | null): void { + if (!template.context || newList === null) { + return; + } + + this._onDidChangeSetting.fire({ + key: template.context.setting.key, + value: newList, + type: template.context.valueType + }); + } + + private computeNewList(template: ISettingListItemTemplate, e: IListChangeEvent): string[] | undefined | null { if (template.context) { - let newValue: any[] = []; + let newValue: string[] = []; if (isArray(template.context.scopeValue)) { newValue = [...template.context.scopeValue]; } else if (isArray(template.context.value)) { @@ -732,29 +757,30 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr template.context.defaultValue.length === newValue.length && template.context.defaultValue.join() === newValue.join() ) { - return this._onDidChangeSetting.fire({ - key: template.context.setting.key, - value: undefined, // reset setting - type: template.context.valueType - }); + return undefined; } - this._onDidChangeSetting.fire({ - key: template.context.setting.key, - value: newValue, - type: template.context.valueType - }); + return newValue; } + + return undefined; } renderElement(element: ITreeNode, index: number, templateData: ISettingListItemTemplate): void { super.renderSettingElement(element, index, templateData); } - protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string) => void): void { + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string[] | undefined) => void): void { const value = getListDisplayValue(dataElement); template.listWidget.setValue(value); template.context = dataElement; + + template.onChange = (v) => { + onChange(v); + renderArrayValidations(dataElement, template, v, false); + }; + + renderArrayValidations(dataElement, template, value.map(v => v.value), true); } } @@ -1237,6 +1263,29 @@ function renderValidations(dataElement: SettingsTreeSettingElement, template: IS DOM.removeClass(template.containerElement, 'invalid-input'); } +function renderArrayValidations( + dataElement: SettingsTreeSettingElement, + template: ISettingListItemTemplate, + value: string[] | undefined, + calledOnStartup: boolean +) { + DOM.addClass(template.containerElement, 'invalid-input'); + if (dataElement.setting.validator) { + const errMsg = dataElement.setting.validator(value); + if (errMsg && errMsg !== '') { + DOM.addClass(template.containerElement, 'invalid-input'); + template.validationErrorMessageElement.innerText = errMsg; + const validationError = localize('validationError', "Validation Error."); + template.containerElement.setAttribute('aria-label', [dataElement.setting.key, validationError, errMsg].join(' ')); + if (!calledOnStartup) { ariaAlert(validationError + ' ' + errMsg); } + return; + } else { + template.containerElement.setAttribute('aria-label', dataElement.setting.key); + DOM.removeClass(template.containerElement, 'invalid-input'); + } + } +} + function cleanRenderedMarkdown(element: Node): void { for (let i = 0; i < element.childNodes.length; i++) { const child = element.childNodes.item(i); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index c85026c360b5d..efe952907cd15 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -9,12 +9,12 @@ import { isArray, withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { ITOCEntry, knownAcronyms, knownTermMappings } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices'; @@ -227,20 +227,24 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { matchesScope(scope: SettingsTarget, isRemote: boolean): boolean { const configTarget = URI.isUri(scope) ? ConfigurationTarget.WORKSPACE_FOLDER : scope; + if (!this.setting.scope) { + return true; + } + if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) { - return this.setting.scope === ConfigurationScope.RESOURCE; + return FOLDER_SCOPES.indexOf(this.setting.scope) !== -1; } if (configTarget === ConfigurationTarget.WORKSPACE) { - return this.setting.scope === ConfigurationScope.WINDOW || this.setting.scope === ConfigurationScope.RESOURCE; + return WORKSPACE_SCOPES.indexOf(this.setting.scope) !== -1; } if (configTarget === ConfigurationTarget.USER_REMOTE) { - return this.setting.scope === ConfigurationScope.MACHINE || this.setting.scope === ConfigurationScope.WINDOW || this.setting.scope === ConfigurationScope.RESOURCE; + return REMOTE_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; } if (configTarget === ConfigurationTarget.USER_LOCAL && isRemote) { - return this.setting.scope !== ConfigurationScope.MACHINE; + return LOCAL_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; } return true; diff --git a/src/vs/workbench/contrib/preferences/browser/tocTree.ts b/src/vs/workbench/contrib/preferences/browser/tocTree.ts index 93a335ee145ec..5c057d629c463 100644 --- a/src/vs/workbench/contrib/preferences/browser/tocTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/tocTree.ts @@ -115,6 +115,7 @@ export class TOCRenderer implements ITreeRenderer; defineWhenExpression(keybindingEntry: IKeybindingItemEntry): void; diff --git a/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts b/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts index abde827728c35..3dcbb07c16c52 100644 --- a/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts @@ -101,18 +101,17 @@ class GotoLineEntry extends EditorQuickOpenEntry { if (this.editorService.activeTextEditorWidget && this.invalidRange(maxLineNumber)) { const position = this.editorService.activeTextEditorWidget.getPosition(); if (position) { - const currentLine = position.lineNumber; if (maxLineNumber > 0) { - return nls.localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}. Type a line number between 1 and {1} to navigate to.", currentLine, maxLineNumber); + return nls.localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Column: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, maxLineNumber); } - return nls.localize('gotoLineLabelEmpty', "Current Line: {0}. Type a line number to navigate to.", currentLine); + return nls.localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column); } } // Input valid, indicate action - return this.column ? nls.localize('gotoLineColumnLabel', "Go to line {0} and character {1}.", this.line, this.column) : nls.localize('gotoLineLabel', "Go to line {0}.", this.line); + return this.column ? nls.localize('gotoLineColumnLabel', "Go to line {0} and column {1}.", this.line, this.column) : nls.localize('gotoLineLabel', "Go to line {0}.", this.line); } private invalidRange(maxLineNumber: number = this.getMaxLineNumber()): boolean { @@ -231,8 +230,7 @@ export class GotoLineHandler extends QuickOpenHandler { if (this.editorService.activeTextEditorWidget) { const position = this.editorService.activeTextEditorWidget.getPosition(); if (position) { - const currentLine = position.lineNumber; - return nls.localize('gotoLineLabelEmpty', "Current Line: {0}. Type a line number to navigate to.", currentLine); + return nls.localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column); } } diff --git a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts index eef3c303d6c36..1742ba33738b6 100644 --- a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts @@ -16,15 +16,15 @@ import { IModelDecorationsChangeAccessor, OverviewRulerLane, IModelDeltaDecorati import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; -import { DocumentSymbolProviderRegistry, DocumentSymbol, symbolKindToCssClass, SymbolKind } from 'vs/editor/common/modes'; +import { DocumentSymbolProviderRegistry, DocumentSymbol, symbolKindToCssClass, SymbolKind, SymbolTag } from 'vs/editor/common/modes'; import { IRange } from 'vs/editor/common/core/range'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; import { GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { asPromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; export const GOTO_SYMBOL_PREFIX = '@'; export const SCOPE_PREFIX = ':'; @@ -73,10 +73,8 @@ class OutlineModel extends QuickOpenModel { applyFilter(searchValue: string): void { // Normalize search - let normalizedSearchValue = searchValue; - if (searchValue.indexOf(SCOPE_PREFIX) === 0) { - normalizedSearchValue = normalizedSearchValue.substr(SCOPE_PREFIX.length); - } + const searchValueLow = searchValue.toLowerCase(); + const searchValuePos = searchValue.indexOf(SCOPE_PREFIX) === 0 ? 1 : 0; // Check for match and update visibility and group label this.entries.forEach((entry: SymbolEntry) => { @@ -84,34 +82,24 @@ class OutlineModel extends QuickOpenModel { // Clear all state first entry.setGroupLabel(undefined); entry.setShowBorder(false); - entry.setHighlights([]); + entry.setScore(undefined); entry.setHidden(false); // Filter by search - if (normalizedSearchValue) { - const highlights = filters.matchesFuzzy2(normalizedSearchValue, entry.getLabel()); - if (highlights) { - entry.setHighlights(highlights); - entry.setHidden(false); - } else if (!entry.isHidden()) { - entry.setHidden(true); - } + if (searchValue.length > searchValuePos) { + const score = filters.fuzzyScore( + searchValue.substr(searchValuePos), searchValueLow.substr(searchValuePos), 0, + entry.getLabel(), entry.getLabel().toLowerCase(), 0, + true + ); + entry.setScore(score); + entry.setHidden(!score); } }); - // Sort properly if actually searching - if (searchValue) { - if (searchValue.indexOf(SCOPE_PREFIX) === 0) { - this.entries.sort(this.sortScoped.bind(this, searchValue.toLowerCase())); - } else { - this.entries.sort(this.sortNormal.bind(this, searchValue.toLowerCase())); - } - } + this.entries.sort(SymbolEntry.compareByRank); + - // Otherwise restore order as appearing in outline - else { - this.entries.sort((a: SymbolEntry, b: SymbolEntry) => a.getIndex() - b.getIndex()); - } // Mark all type groups const visibleResults = this.getEntries(true); @@ -156,74 +144,6 @@ class OutlineModel extends QuickOpenModel { } } - private sortNormal(searchValue: string, elementA: SymbolEntry, elementB: SymbolEntry): number { - - // Handle hidden elements - if (elementA.isHidden() && elementB.isHidden()) { - return 0; - } else if (elementA.isHidden()) { - return 1; - } else if (elementB.isHidden()) { - return -1; - } - - const elementAName = elementA.getLabel().toLowerCase(); - const elementBName = elementB.getLabel().toLowerCase(); - - // Compare by name - const r = elementAName.localeCompare(elementBName); - if (r !== 0) { - return r; - } - - // If name identical sort by range instead - const elementARange = elementA.getRange(); - const elementBRange = elementB.getRange(); - - return elementARange.startLineNumber - elementBRange.startLineNumber; - } - - private sortScoped(searchValue: string, elementA: SymbolEntry, elementB: SymbolEntry): number { - - // Handle hidden elements - if (elementA.isHidden() && elementB.isHidden()) { - return 0; - } else if (elementA.isHidden()) { - return 1; - } else if (elementB.isHidden()) { - return -1; - } - - // Remove scope char - searchValue = searchValue.substr(SCOPE_PREFIX.length); - - // Sort by type first if scoped search - const elementATypeLabel = NLS_SYMBOL_KIND_CACHE[elementA.getKind()] || FALLBACK_NLS_SYMBOL_KIND; - const elementBTypeLabel = NLS_SYMBOL_KIND_CACHE[elementB.getKind()] || FALLBACK_NLS_SYMBOL_KIND; - let r = elementATypeLabel.localeCompare(elementBTypeLabel); - if (r !== 0) { - return r; - } - - // Special sort when searching in scoped mode - if (searchValue) { - const elementAName = elementA.getLabel().toLowerCase(); - const elementBName = elementB.getLabel().toLowerCase(); - - // Compare by name - r = elementAName.localeCompare(elementBName); - if (r !== 0) { - return r; - } - } - - // Default to sort by range - const elementARange = elementA.getRange(); - const elementBRange = elementB.getRange(); - - return elementARange.startLineNumber - elementBRange.startLineNumber; - } - private renderGroupLabel(type: SymbolKind, count: number): string { let pattern = NLS_SYMBOL_KIND_CACHE[type]; if (!pattern) { @@ -235,33 +155,26 @@ class OutlineModel extends QuickOpenModel { } class SymbolEntry extends EditorQuickOpenEntryGroup { - private editorService: IEditorService; - private index: number; - private name: string; - private kind: SymbolKind; - private icon: string; - private description: string; - private range: IRange; - private revealRange: IRange; - private handler: GotoSymbolHandler; - - constructor(index: number, name: string, kind: SymbolKind, description: string, icon: string, range: IRange, revealRange: IRange, highlights: IHighlight[], editorService: IEditorService, handler: GotoSymbolHandler) { - super(); - this.index = index; - this.name = name; - this.kind = kind; - this.icon = icon; - this.description = description; - this.range = range; - this.revealRange = revealRange || range; - this.setHighlights(highlights); - this.editorService = editorService; - this.handler = handler; + private score?: filters.FuzzyScore; + + constructor( + private readonly index: number, + private readonly name: string, + private readonly kind: SymbolKind, + private readonly description: string, + private readonly icon: string, + private readonly deprecated: boolean, + private readonly range: IRange, + private readonly revealRange: IRange, + private readonly editorService: IEditorService, + private readonly handler: GotoSymbolHandler + ) { + super(); } - getIndex(): number { - return this.index; + setScore(score: filters.FuzzyScore | undefined): void { + this.score = score; } getLabel(): string { @@ -276,6 +189,18 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { return this.icon; } + getLabelOptions(): IIconLabelValueOptions | undefined { + return this.deprecated ? { extraClasses: ['deprecated'] } : undefined; + } + + getHighlights(): [IHighlight[], IHighlight[] | undefined, IHighlight[] | undefined] { + return [ + this.deprecated ? [] : filters.createMatches(this.score), + undefined, + undefined + ]; + } + getDescription(): string { return this.description; } @@ -294,7 +219,7 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { getOptions(pinned?: boolean): ITextEditorOptions { return { - selection: this.toSelection(), + selection: this.revealRange, pinned }; } @@ -317,7 +242,7 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { // Apply selection and focus else { - const range = this.toSelection(); + const range = this.revealRange; const activeTextEditorWidget = this.editorService.activeTextEditorWidget; if (activeTextEditorWidget) { activeTextEditorWidget.setSelection(range); @@ -331,7 +256,7 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { private runPreview(): boolean { // Select Outline Position - const range = this.toSelection(); + const range = this.revealRange; const activeTextEditorWidget = this.editorService.activeTextEditorWidget; if (activeTextEditorWidget) { activeTextEditorWidget.revealRangeInCenter(range, ScrollType.Smooth); @@ -345,13 +270,36 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { return false; } - private toSelection(): IRange { - return { - startLineNumber: this.revealRange.startLineNumber, - startColumn: this.revealRange.startColumn || 1, - endLineNumber: this.revealRange.startLineNumber, - endColumn: this.revealRange.startColumn || 1 - }; + static compareByRank(a: SymbolEntry, b: SymbolEntry): number { + if (!a.score && b.score) { + return 1; + } else if (a.score && !b.score) { + return -1; + } + if (a.score && b.score) { + if (a.score[0] > b.score[0]) { + return -1; + } else if (a.score[0] < b.score[0]) { + return 1; + } + } + if (a.index < b.index) { + return -1; + } else if (a.index > b.index) { + return 1; + } + return 0; + } + + static compareByKindAndRank(a: SymbolEntry, b: SymbolEntry): number { + // Sort by type first if scoped search + const kindA = NLS_SYMBOL_KIND_CACHE[a.getKind()] || FALLBACK_NLS_SYMBOL_KIND; + const kindB = NLS_SYMBOL_KIND_CACHE[b.getKind()] || FALLBACK_NLS_SYMBOL_KIND; + let r = kindA.localeCompare(kindB); + if (r === 0) { + r = this.compareByRank(a, b); + } + return r; } } @@ -466,11 +414,11 @@ export class GotoSymbolHandler extends QuickOpenHandler { }; } - private toQuickOpenEntries(flattened: DocumentSymbol[]): SymbolEntry[] { + private toQuickOpenEntries(symbols: DocumentSymbol[]): SymbolEntry[] { const results: SymbolEntry[] = []; - for (let i = 0; i < flattened.length; i++) { - const element = flattened[i]; + for (let i = 0; i < symbols.length; i++) { + const element = symbols[i]; const label = strings.trim(element.name); // Show parent scope as description @@ -479,8 +427,8 @@ export class GotoSymbolHandler extends QuickOpenHandler { // Add results.push(new SymbolEntry(i, - label, element.kind, description, `symbol-icon ${icon}`, - element.range, element.selectionRange, [], this.editorService, this + label, element.kind, description, `symbol-icon ${icon}`, element.tags && element.tags.indexOf(SymbolTag.Deprecated) >= 0, + element.range, element.selectionRange, this.editorService, this )); } @@ -504,7 +452,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { } if (model && types.isFunction((model).getLanguageIdentifier)) { - const entries = await asPromise(() => getDocumentSymbols(model, true, this.pendingOutlineRequest!.token)); + const entries = await getDocumentSymbols(model, true, this.pendingOutlineRequest!.token); return new OutlineModel(this.toQuickOpenEntries(entries)); } diff --git a/src/vs/workbench/contrib/relauncher/common/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/common/relauncher.contribution.ts index d3c0d4f064f69..2ec3089894f45 100644 --- a/src/vs/workbench/contrib/relauncher/common/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/common/relauncher.contribution.ts @@ -159,7 +159,7 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor constructor( @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IExtensionService extensionService: IExtensionService, - @IWindowService windowSevice: IWindowService, + @IWindowService windowService: IWindowService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { super(); @@ -170,7 +170,7 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor } if (environmentService.configuration.remoteAuthority) { - windowSevice.reloadWindow(); // TODO@aeschli, workaround + windowService.reloadWindow(); // TODO@aeschli, workaround } else { extensionService.restartExtensionHost(); } diff --git a/src/vs/workbench/contrib/remote/browser/help-documentation-dark.svg b/src/vs/workbench/contrib/remote/browser/help-documentation-dark.svg new file mode 100644 index 0000000000000..2673902c68405 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-documentation-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-documentation-hc.svg b/src/vs/workbench/contrib/remote/browser/help-documentation-hc.svg new file mode 100644 index 0000000000000..e8dc8205bab38 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-documentation-hc.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-documentation-light.svg b/src/vs/workbench/contrib/remote/browser/help-documentation-light.svg new file mode 100644 index 0000000000000..4a3009baeee47 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-documentation-light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-feedback-dark.svg b/src/vs/workbench/contrib/remote/browser/help-feedback-dark.svg new file mode 100644 index 0000000000000..5d99408934e83 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-feedback-dark.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-feedback-hc.svg b/src/vs/workbench/contrib/remote/browser/help-feedback-hc.svg new file mode 100644 index 0000000000000..941430e9dd6a9 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-feedback-hc.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-feedback-light.svg b/src/vs/workbench/contrib/remote/browser/help-feedback-light.svg new file mode 100644 index 0000000000000..72437202b7252 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-feedback-light.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-getting-started-dark.svg b/src/vs/workbench/contrib/remote/browser/help-getting-started-dark.svg new file mode 100644 index 0000000000000..0ea65d83198b2 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-getting-started-dark.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-getting-started-hc.svg b/src/vs/workbench/contrib/remote/browser/help-getting-started-hc.svg new file mode 100644 index 0000000000000..5bb05d3d8c550 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-getting-started-hc.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-getting-started-light.svg b/src/vs/workbench/contrib/remote/browser/help-getting-started-light.svg new file mode 100644 index 0000000000000..46cde7f7cc051 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-getting-started-light.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-report-issue-dark.svg b/src/vs/workbench/contrib/remote/browser/help-report-issue-dark.svg new file mode 100644 index 0000000000000..0117ceb7ded96 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-report-issue-dark.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-report-issue-hc.svg b/src/vs/workbench/contrib/remote/browser/help-report-issue-hc.svg new file mode 100644 index 0000000000000..b0c521b7dc6b1 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-report-issue-hc.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-report-issue-light.svg b/src/vs/workbench/contrib/remote/browser/help-report-issue-light.svg new file mode 100644 index 0000000000000..5da9322b6a931 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-report-issue-light.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-review-issues-dark.svg b/src/vs/workbench/contrib/remote/browser/help-review-issues-dark.svg new file mode 100644 index 0000000000000..21eec9cbcb85f --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-review-issues-dark.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-review-issues-hc.svg b/src/vs/workbench/contrib/remote/browser/help-review-issues-hc.svg new file mode 100644 index 0000000000000..94013ea52ae7b --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-review-issues-hc.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/help-review-issues-light.svg b/src/vs/workbench/contrib/remote/browser/help-review-issues-light.svg new file mode 100644 index 0000000000000..826d0eefbf476 --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/help-review-issues-light.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/vs/workbench/contrib/remote/browser/remote-activity-bar.svg b/src/vs/workbench/contrib/remote/browser/remote-activity-bar.svg new file mode 100644 index 0000000000000..029e6b051c28a --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/remote-activity-bar.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts new file mode 100644 index 0000000000000..eb7bc581f9d5f --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -0,0 +1,423 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./remoteViewlet'; +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/contrib/remote/common/remote.contribution'; +import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IViewDescriptor, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { Event } from 'vs/base/common/event'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; + +interface HelpInformation { + extensionDescription: IExtensionDescription; + getStarted?: string; + documentation?: string; + feedback?: string; + issues?: string; +} + +const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'remoteHelp', + jsonSchema: { + description: nls.localize('RemoteHelpInformationExtPoint', 'Contributes help information for Remote'), + type: 'object', + properties: { + 'getStarted': { + description: nls.localize('RemoteHelpInformationExtPoint.getStarted', "The url to your project's Getting Started page"), + type: 'string' + }, + 'documentation': { + description: nls.localize('RemoteHelpInformationExtPoint.documentation', "The url to your project's documentation page"), + type: 'string' + }, + 'feedback': { + description: nls.localize('RemoteHelpInformationExtPoint.feedback', "The url to your project's feedback reporter"), + type: 'string' + }, + 'issues': { + description: nls.localize('RemoteHelpInformationExtPoint.issues', "The url to your project's issues list"), + type: 'string' + } + } + } +}); + +interface IViewModel { + helpInformations: HelpInformation[]; +} + +class HelpTreeVirtualDelegate implements IListVirtualDelegate { + getHeight(element: IHelpItem): number { + return 22; + } + + getTemplateId(element: IHelpItem): string { + return 'HelpItemTemplate'; + } +} + +interface IHelpItemTemplateData { + parent: HTMLElement; + icon: HTMLElement; +} + +class HelpTreeRenderer implements ITreeRenderer { + templateId: string = 'HelpItemTemplate'; + + renderTemplate(container: HTMLElement): IHelpItemTemplateData { + dom.addClass(container, 'remote-help-tree-node-item'); + + const icon = dom.append(container, dom.$('.remote-help-tree-node-item-icon')); + + const data = Object.create(null); + data.parent = container; + data.icon = icon; + + return data; + } + + renderElement(element: ITreeNode, index: number, templateData: IHelpItemTemplateData, height: number | undefined): void { + const container = templateData.parent; + dom.append(container, templateData.icon); + dom.addClass(templateData.icon, element.element.key); + const labelContainer = dom.append(container, dom.$('.help-item-label')); + labelContainer.innerText = element.element.label; + } + + disposeTemplate(templateData: IHelpItemTemplateData): void { + + } +} + +class HelpDataSource implements IAsyncDataSource { + hasChildren(element: any) { + return element instanceof HelpModel; + } + + getChildren(element: any) { + if (element instanceof HelpModel) { + return element.items; + } + + return []; + } +} + +interface IHelpItem { + key: string; + label: string; + handleClick(): Promise; +} + +class HelpItem implements IHelpItem { + constructor( + public key: string, + public label: string, + public values: { extensionDescription: IExtensionDescription; url: string }[], + private openerService: IOpenerService, + private quickInputService: IQuickInputService + ) { + } + + async handleClick() { + if (this.values.length > 1) { + let actions = this.values.map(value => { + return { + label: value.extensionDescription.displayName || value.extensionDescription.identifier.value, + description: value.url + }; + }); + + const action = await this.quickInputService.pick(actions, { placeHolder: nls.localize('pickRemoteExtension', "Select url to open") }); + + if (action) { + await this.openerService.open(URI.parse(action.label)); + } + } else { + await this.openerService.open(URI.parse(this.values[0].url)); + } + } +} + +class IssueReporterItem implements IHelpItem { + constructor( + public key: string, + public label: string, + public extensionDescriptions: IExtensionDescription[], + private quickInputService: IQuickInputService, + private commandService: ICommandService + ) { + } + + async handleClick() { + if (this.extensionDescriptions.length > 1) { + let actions = this.extensionDescriptions.map(extension => { + return { + label: extension.displayName || extension.identifier.value, + identifier: extension.identifier + }; + }); + + const action = await this.quickInputService.pick(actions, { placeHolder: nls.localize('pickRemoteExtensionToReportIssue', "Select an extension to report issue") }); + + if (action) { + await this.commandService.executeCommand('workbench.action.openIssueReporter', [action.identifier.value]); + } + } else { + await this.commandService.executeCommand('workbench.action.openIssueReporter', [this.extensionDescriptions[0].identifier.value]); + } + } +} + +class HelpModel { + items: IHelpItem[]; + + constructor( + viewModel: IViewModel, + openerService: IOpenerService, + quickInputService: IQuickInputService, + commandService: ICommandService + ) { + let helpItems: IHelpItem[] = []; + const getStarted = viewModel.helpInformations.filter(info => info.getStarted); + + if (getStarted.length) { + helpItems.push(new HelpItem( + 'getStarted', + nls.localize('remote.help.getStarted', "Get Started"), + getStarted.map((info: HelpInformation) => ({ + extensionDescription: info.extensionDescription, + url: info.getStarted! + })), + openerService, + quickInputService + )); + } + + const documentation = viewModel.helpInformations.filter(info => info.documentation); + + if (documentation.length) { + helpItems.push(new HelpItem( + 'documentation', + nls.localize('remote.help.documentation', "Read Documentation"), + documentation.map((info: HelpInformation) => ({ + extensionDescription: info.extensionDescription, + url: info.documentation! + })), + openerService, + quickInputService + )); + } + + const feedback = viewModel.helpInformations.filter(info => info.feedback); + + if (feedback.length) { + helpItems.push(new HelpItem( + 'feedback', + nls.localize('remote.help.feedback', "Provide Feedback"), + feedback.map((info: HelpInformation) => ({ + extensionDescription: info.extensionDescription, + url: info.feedback! + })), + openerService, + quickInputService + )); + } + + const issues = viewModel.helpInformations.filter(info => info.issues); + + if (issues.length) { + helpItems.push(new HelpItem( + 'issues', + nls.localize('remote.help.issues', "Review Issues"), + issues.map((info: HelpInformation) => ({ + extensionDescription: info.extensionDescription, + url: info.issues! + })), + openerService, + quickInputService + )); + } + + if (helpItems.length) { + helpItems.push(new IssueReporterItem( + 'issueReporter', + nls.localize('remote.help.report', "Report Issue"), + viewModel.helpInformations.map(info => info.extensionDescription), + quickInputService, + commandService + )); + } + + if (helpItems.length) { + this.items = helpItems; + } + } +} + +class HelpPanel extends ViewletPanel { + static readonly ID = '~remote.helpPanel'; + static readonly TITLE = nls.localize('remote.help', "Help and feedback"); + private tree!: WorkbenchAsyncDataTree; + + constructor( + protected viewModel: IViewModel, + options: IViewletPanelOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IConfigurationService protected configurationService: IConfigurationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IOpenerService protected openerService: IOpenerService, + @IQuickInputService protected quickInputService: IQuickInputService, + @ICommandService protected commandService: ICommandService + + + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + } + + protected renderBody(container: HTMLElement): void { + dom.addClass(container, 'remote-help'); + const treeContainer = document.createElement('div'); + dom.addClass(treeContainer, 'remote-help-content'); + container.appendChild(treeContainer); + + this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, + treeContainer, + new HelpTreeVirtualDelegate(), + [new HelpTreeRenderer()], + new HelpDataSource(), + { + keyboardSupport: true, + } + ); + + const model = new HelpModel(this.viewModel, this.openerService, this.quickInputService, this.commandService); + + this.tree.setInput(model); + + const helpItemNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: false, openOnSelection: false })); + + this._register(Event.debounce(helpItemNavigator.onDidOpenResource, (last, event) => event, 75, true)(e => { + e.element.handleClick(); + })); + } + + protected layoutBody(height: number, width: number): void { + this.tree.layout(height, width); + } +} + +class HelpPanelDescriptor implements IViewDescriptor { + readonly id = HelpPanel.ID; + readonly name = HelpPanel.TITLE; + readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly canToggleVisibility = true; + readonly hideByDefault = false; + readonly workspace = true; + + constructor(viewModel: IViewModel) { + this.ctorDescriptor = { ctor: HelpPanel, arguments: [viewModel] }; + } +} + + +export class RemoteViewlet extends ViewContainerViewlet implements IViewModel { + private helpPanelDescriptor = new HelpPanelDescriptor(this); + + helpInformations: HelpInformation[] = []; + + constructor( + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService + ) { + super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + + remoteHelpExtPoint.setHandler((extensions) => { + let helpInformation: HelpInformation[] = []; + for (let extension of extensions) { + this._handleRemoteInfoExtensionPoint(extension, helpInformation); + } + + this.helpInformations = helpInformation; + + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + if (this.helpInformations.length) { + viewsRegistry.registerViews([this.helpPanelDescriptor], VIEW_CONTAINER); + } else { + viewsRegistry.deregisterViews([this.helpPanelDescriptor], VIEW_CONTAINER); + } + }); + } + + private _handleRemoteInfoExtensionPoint(extension: IExtensionPointUser, helpInformation: HelpInformation[]) { + if (!extension.description.enableProposedApi) { + return; + } + + if (!extension.value.documentation && !extension.value.feedback && !extension.value.getStarted && !extension.value.issues) { + return; + } + + helpInformation.push({ + extensionDescription: extension.description, + getStarted: extension.value.getStarted, + documentation: extension.value.documentation, + feedback: extension.value.feedback, + issues: extension.value.issues + }); + } + + onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { + // too late, already added to the view model + return super.onDidAddViews(added); + } + + getTitle(): string { + const title = nls.localize('remote.explorer', "Remote Explorer"); + return title; + } +} + +Registry.as(ViewletExtensions.Viewlets).registerViewlet(new ViewletDescriptor( + RemoteViewlet, + VIEWLET_ID, + nls.localize('remote.explorer', "Remote Explorer"), + 'remote', + 4 +)); diff --git a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css new file mode 100644 index 0000000000000..86bd4b76dc9cd --- /dev/null +++ b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .activitybar>.content .monaco-action-bar .action-label.remote { + -webkit-mask: url('remote-activity-bar.svg') no-repeat 50% 50%; +} + +.remote-help-content .monaco-list .monaco-list-row .remote-help-tree-node-item { + display: flex; + height: 22px; + line-height: 22px; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + flex-wrap: nowrap; +} + +.remote-help-content .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon { + background-size: 16px; + background-position: left center; + background-repeat: no-repeat; + padding-right: 6px; + width: 16px; + height: 22px; + -webkit-font-smoothing: antialiased; +} + +.remote-help-content .monaco-list .monaco-list-row .monaco-tl-twistie { + width: 0px !important; +} + +.vs .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.getStarted { + background-image: url('help-getting-started-light.svg') +} + +.vs .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.documentation { + background-image: url('help-documentation-light.svg') +} + +.vs .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.feedback { + background-image: url('help-feedback-light.svg') +} + +.vs .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issues { + background-image: url('help-review-issues-light.svg') +} + +.vs .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issueReporter { + background-image: url('help-report-issue-light.svg') +} + +.vs-dark .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.getStarted { + background-image: url('help-getting-started-dark.svg') +} + +.vs-dark .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.documentation { + background-image: url('help-documentation-dark.svg') +} + +.vs-dark .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.feedback { + background-image: url('help-feedback-dark.svg') +} + +.vs-dark .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issues { + background-image: url('help-review-issues-dark.svg') +} + +.vs-dark .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issueReporter { + background-image: url('help-report-issue-dark.svg') +} + +.hc-black .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.getStarted { + background-image: url('help-getting-started-hc.svg') +} + +.hc-black .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.documentation { + background-image: url('help-documentation-hc.svg') +} + +.hc-black .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.feedback { + background-image: url('help-feedback-hc.svg') +} + +.hc-black .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issues { + background-image: url('help-review-issues-hc.svg') +} + +.hc-black .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon.issueReporter { + background-image: url('help-report-issue-hc.svg') +} diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 9235c739fb014..404f5a2a78d0f 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -7,8 +7,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ILabelService } from 'vs/platform/label/common/label'; -import { isWeb, OperatingSystem } from 'vs/base/common/platform'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { OperatingSystem } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { IRemoteAgentService, RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,31 +16,56 @@ import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/c import { localize } from 'vs/nls'; import { joinPath } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; + +export const VIEWLET_ID = 'workbench.view.remote'; +export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer( + VIEWLET_ID, + true, + undefined, + { + getOrder: (group?: string) => { + if (!group) { + return; + } + + let matches = /^targets@(\d+)$/.exec(group); + if (matches) { + return -1000; + } + + matches = /^details@(\d+)$/.exec(group); + + if (matches) { + return -500; + } + + return; + } + } +); export class LabelContribution implements IWorkbenchContribution { constructor( @ILabelService private readonly labelService: ILabelService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService) { this.registerFormatters(); } private registerFormatters(): void { - if (isWeb) { - this.remoteAgentService.getEnvironment().then(remoteEnvironment => { - if (remoteEnvironment) { - this.labelService.registerFormatter({ - scheme: Schemas.vscodeRemote, - authority: this.environmentService.configuration.remoteAuthority, - formatting: { - label: '${path}', - separator: remoteEnvironment.os === OperatingSystem.Windows ? '\\' : '/', - tildify: remoteEnvironment.os !== OperatingSystem.Windows - } - }); - } - }); - } + this.remoteAgentService.getEnvironment().then(remoteEnvironment => { + if (remoteEnvironment) { + this.labelService.registerFormatter({ + scheme: Schemas.vscodeRemote, + formatting: { + label: '${path}', + separator: remoteEnvironment.os === OperatingSystem.Windows ? '\\' : '/', + tildify: remoteEnvironment.os !== OperatingSystem.Windows, + normalizeDriveLetter: remoteEnvironment.os === OperatingSystem.Windows + } + }); + } + }); } } diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 99f4eefd121b1..857e42cd54c5c 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -27,10 +27,10 @@ import { DialogChannel } from 'vs/platform/dialogs/node/dialogIpc'; import { DownloadServiceChannel } from 'vs/platform/download/common/downloadIpc'; import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc'; import { ipcRenderer as ipc } from 'electron'; -import { IDiagnosticInfoOptions, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IDiagnosticInfoOptions, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProgressService, IProgress, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; +import { PersistentConnectionEventType, ReconnectionWaitEvent } from 'vs/platform/remote/common/remoteAgentConnection'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import Severity from 'vs/base/common/severity'; @@ -309,46 +309,111 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IProgressService progressService: IProgressService, @IDialogService dialogService: IDialogService, - @ICommandService commandService: ICommandService + @ICommandService commandService: ICommandService, + @IContextKeyService contextKeyService: IContextKeyService ) { const connection = remoteAgentService.getConnection(); if (connection) { let currentProgressPromiseResolve: (() => void) | null = null; let progressReporter: ProgressReporter | null = null; + let lastLocation: ProgressLocation | null = null; let currentTimer: ReconnectionTimer | null = null; + let reconnectWaitEvent: ReconnectionWaitEvent | null = null; + let disposableListener: IDisposable | null = null; + + function showProgress(location: ProgressLocation, buttons?: string[]) { + if (currentProgressPromiseResolve) { + currentProgressPromiseResolve(); + } + + const promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); + lastLocation = location; + + if (location === ProgressLocation.Dialog) { + // Show dialog + progressService!.withProgress( + { location: ProgressLocation.Dialog, buttons }, + (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, + (choice?) => { + // Handle choice from dialog + if (choice === 0 && buttons && reconnectWaitEvent) { + reconnectWaitEvent.skipWait(); + } else { + showProgress(ProgressLocation.Notification, buttons); + } + + progressReporter!.report(); + }); + } else { + // Show notification + progressService!.withProgress( + { location: ProgressLocation.Notification, buttons }, + (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, + (choice?) => { + // Handle choice from notification + if (choice === 0 && buttons && reconnectWaitEvent) { + reconnectWaitEvent.skipWait(); + } else { + hideProgress(); + } + }); + } + } + + function hideProgress() { + if (currentProgressPromiseResolve) { + currentProgressPromiseResolve(); + } + + currentProgressPromiseResolve = null; + } connection.onDidStateChange((e) => { if (currentTimer) { currentTimer.dispose(); currentTimer = null; } + + if (disposableListener) { + disposableListener.dispose(); + disposableListener = null; + } switch (e.type) { case PersistentConnectionEventType.ConnectionLost: if (!currentProgressPromiseResolve) { - let promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); - progressService!.withProgress( - { location: ProgressLocation.Dialog }, - (progress: IProgress | null) => { progressReporter = new ProgressReporter(progress!); return promise; }, - () => { - currentProgressPromiseResolve!(); - promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); - progressService!.withProgress({ location: ProgressLocation.Notification }, (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }); - progressReporter!.report(); - } - ); + progressReporter = new ProgressReporter(null); + showProgress(ProgressLocation.Dialog, [nls.localize('reconnectNow', "Reconnect Now")]); } progressReporter!.report(nls.localize('connectionLost', "Connection Lost")); break; case PersistentConnectionEventType.ReconnectionWait: + hideProgress(); + reconnectWaitEvent = e; + showProgress(lastLocation || ProgressLocation.Notification, [nls.localize('reconnectNow', "Reconnect Now")]); currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds); break; case PersistentConnectionEventType.ReconnectionRunning: + hideProgress(); + showProgress(lastLocation || ProgressLocation.Notification); progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect...")); + + // Register to listen for quick input is opened + disposableListener = contextKeyService.onDidChangeContext((contextKeyChangeEvent) => { + const reconnectInteraction = new Set(['inQuickOpen']); + if (contextKeyChangeEvent.affectsSome(reconnectInteraction)) { + // Need to move from dialog if being shown and user needs to type in a prompt + if (lastLocation === ProgressLocation.Dialog && progressReporter !== null) { + hideProgress(); + showProgress(ProgressLocation.Notification); + progressReporter.report(); + } + } + }); + break; case PersistentConnectionEventType.ReconnectionPermanentFailure: - currentProgressPromiseResolve!(); - currentProgressPromiseResolve = null; + hideProgress(); progressReporter = null; dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1 }).then(choice => { @@ -359,8 +424,7 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { }); break; case PersistentConnectionEventType.ConnectionGain: - currentProgressPromiseResolve!(); - currentProgressPromiseResolve = null; + hideProgress(); progressReporter = null; break; } diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts index 48b238d10f338..3534ef147dfd5 100644 --- a/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts @@ -34,7 +34,7 @@ self.addEventListener('activate', event => { //#region --- fetching/caching const _cacheName = 'vscode-extension-resources'; -const _resourcePrefix = '/vscode-remote'; +const _resourcePrefix = '/vscode-remote-resource'; const _pendingFetch = new Map(); self.addEventListener('message', event => { diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts index 0dc883addcccf..89e7d32eae6d6 100644 --- a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts @@ -10,7 +10,7 @@ // statement. // trigger service worker updates -const _tag = '52278406-3ca9-48af-a8fb-8495add5bb4e'; +const _tag = '23549971-9b8d-41bb-92ae-d7f6a68c9702'; // loader world const baseUrl = '../../../../../'; diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 3181a10d8d1ec..f530bce968ae0 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -60,6 +60,20 @@ display: inline-flex; } +.search-view .search-widget .replace-input { + position: relative; + display: flex; + display: -webkit-flex; + vertical-align: middle; + width: auto !important; +} + +.search-view .search-widget .replace-input > .controls { + position: absolute; + top: 3px; + right: 2px; +} + .search-view .search-widget .replace-container.disabled { display: none; } diff --git a/src/vs/workbench/contrib/search/browser/media/stop-dark.svg b/src/vs/workbench/contrib/search/browser/media/stop-dark.svg index 7e6319104daf0..890af29835444 100644 --- a/src/vs/workbench/contrib/search/browser/media/stop-dark.svg +++ b/src/vs/workbench/contrib/search/browser/media/stop-dark.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/search/browser/media/stop-hc.svg b/src/vs/workbench/contrib/search/browser/media/stop-hc.svg index a879a194c7a9f..1c88dfb60a7b2 100644 --- a/src/vs/workbench/contrib/search/browser/media/stop-hc.svg +++ b/src/vs/workbench/contrib/search/browser/media/stop-hc.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/search/browser/media/stop-light.svg b/src/vs/workbench/contrib/search/browser/media/stop-light.svg index 10d05f5d8ab6d..7e41aeff58966 100644 --- a/src/vs/workbench/contrib/search/browser/media/stop-light.svg +++ b/src/vs/workbench/contrib/search/browser/media/stop-light.svg @@ -1,3 +1,3 @@ - + diff --git a/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts b/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts index 4a0a138d83b33..c0bcedcb22111 100644 --- a/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts @@ -8,13 +8,13 @@ import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ThrottledDelayer } from 'vs/base/common/async'; import { QuickOpenHandler, EditorQuickOpenEntry } from 'vs/workbench/browser/quickopen'; -import { QuickOpenModel, QuickOpenEntry, compareEntries } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { QuickOpenModel, QuickOpenEntry, IHighlight } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; import * as filters from 'vs/base/common/filters'; import * as strings from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import { symbolKindToCssClass } from 'vs/editor/common/modes'; +import { symbolKindToCssClass, SymbolTag } from 'vs/editor/common/modes'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -25,9 +25,12 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; class SymbolEntry extends EditorQuickOpenEntry { - private bearingResolve: Promise | undefined; + + private bearingResolve?: Promise; + private score?: filters.FuzzyScore; constructor( private bearing: IWorkspaceSymbol, @@ -40,6 +43,14 @@ class SymbolEntry extends EditorQuickOpenEntry { super(editorService); } + setScore(score: filters.FuzzyScore | undefined) { + this.score = score; + } + + getHighlights(): [IHighlight[] /* Label */, IHighlight[] | undefined /* Description */, IHighlight[] | undefined /* Detail */] { + return [this.isDeprecated() ? [] : filters.createMatches(this.score), undefined, undefined]; + } + getLabel(): string { return this.bearing.name; } @@ -65,10 +76,18 @@ class SymbolEntry extends EditorQuickOpenEntry { return symbolKindToCssClass(this.bearing.kind); } + getLabelOptions(): IIconLabelValueOptions | undefined { + return this.isDeprecated() ? { extraClasses: ['deprecated'] } : undefined; + } + getResource(): URI { return this.bearing.location.uri; } + private isDeprecated(): boolean { + return this.bearing.tags ? this.bearing.tags.indexOf(SymbolTag.Deprecated) >= 0 : false; + } + run(mode: Mode, context: IEntryRunContext): boolean { // resolve this type bearing if neccessary @@ -111,18 +130,24 @@ class SymbolEntry extends EditorQuickOpenEntry { return input; } - static compare(elementA: SymbolEntry, elementB: SymbolEntry, searchValue: string): number { - - // Sort by Type if name is identical - const elementAName = elementA.getLabel().toLowerCase(); - const elementBName = elementB.getLabel().toLowerCase(); - if (elementAName === elementBName) { - let elementAType = symbolKindToCssClass(elementA.bearing.kind); - let elementBType = symbolKindToCssClass(elementB.bearing.kind); - return elementAType.localeCompare(elementBType); + static compare(a: SymbolEntry, b: SymbolEntry, searchValue: string): number { + // order: score, name, kind + if (a.score && b.score) { + if (a.score[0] > b.score[0]) { + return -1; + } else if (a.score[0] < b.score[0]) { + return 1; + } } - - return compareEntries(elementA, elementB, searchValue); + const aName = a.getLabel().toLowerCase(); + const bName = b.getLabel().toLowerCase(); + let res = aName.localeCompare(bName); + if (res !== 0) { + return res; + } + let aKind = symbolKindToCssClass(a.bearing.kind); + let bKind = symbolKindToCssClass(b.bearing.kind); + return aKind.localeCompare(bKind); } } @@ -198,6 +223,9 @@ export class OpenSymbolHandler extends QuickOpenHandler { private fillInSymbolEntries(bucket: SymbolEntry[], provider: IWorkspaceSymbolProvider, types: IWorkspaceSymbol[], searchValue: string): void { + const pattern = strings.stripWildcards(searchValue); + const patternLow = pattern.toLowerCase(); + // Convert to Entries for (let element of types) { if (this.options.skipLocalSymbols && !!element.containerName) { @@ -205,7 +233,11 @@ export class OpenSymbolHandler extends QuickOpenHandler { } const entry = this.instantiationService.createInstance(SymbolEntry, element, provider); - entry.setHighlights(filters.matchesFuzzy2(searchValue, entry.getLabel()) || []); + entry.setScore(filters.fuzzyScore( + pattern, patternLow, 0, + entry.getLabel(), entry.getLabel().toLowerCase(), 0, + true + )); bucket.push(entry); } } diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index ca96a4cd1ee76..9eac783512a11 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -7,14 +7,11 @@ import * as DOM from 'vs/base/browser/dom'; import { Action } from 'vs/base/common/actions'; import { INavigator } from 'vs/base/common/iterator'; import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { Schemas } from 'vs/base/common/network'; -import { normalize } from 'vs/base/common/path'; import { isWindows, OS } from 'vs/base/common/platform'; import { repeat } from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ILabelService } from 'vs/platform/label/common/label'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -667,14 +664,11 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { } } -function uriToClipboardString(resource: URI): string { - return resource.scheme === Schemas.file ? normalize(normalizeDriveLetter(resource.fsPath)) : resource.toString(); -} - export const copyPathCommand: ICommandHandler = async (accessor, fileMatch: FileMatch | FolderMatch) => { const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); - const text = uriToClipboardString(fileMatch.resource); + const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); await clipboardService.writeText(text); }; @@ -706,25 +700,26 @@ function matchToString(match: Match, indent = 0): string { } const lineDelimiter = isWindows ? '\r\n' : '\n'; -function fileMatchToString(fileMatch: FileMatch, maxMatches: number): { text: string, count: number } { +function fileMatchToString(fileMatch: FileMatch, maxMatches: number, labelService: ILabelService): { text: string, count: number } { const matchTextRows = fileMatch.matches() .sort(searchMatchComparer) .slice(0, maxMatches) .map(match => matchToString(match, 2)); + const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); return { - text: `${uriToClipboardString(fileMatch.resource)}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, + text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, count: matchTextRows.length }; } -function folderMatchToString(folderMatch: FolderMatch | BaseFolderMatch, maxMatches: number): { text: string, count: number } { +function folderMatchToString(folderMatch: FolderMatch | BaseFolderMatch, maxMatches: number, labelService: ILabelService): { text: string, count: number } { const fileResults: string[] = []; let numMatches = 0; const matches = folderMatch.matches().sort(searchMatchComparer); for (let i = 0; i < folderMatch.fileCount() && numMatches < maxMatches; i++) { - const fileResult = fileMatchToString(matches[i], maxMatches - numMatches); + const fileResult = fileMatchToString(matches[i], maxMatches - numMatches, labelService); numMatches += fileResult.count; fileResults.push(fileResult.text); } @@ -738,14 +733,15 @@ function folderMatchToString(folderMatch: FolderMatch | BaseFolderMatch, maxMatc const maxClipboardMatches = 1e4; export const copyMatchCommand: ICommandHandler = async (accessor, match: RenderableMatch) => { const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); let text: string | undefined; if (match instanceof Match) { text = matchToString(match); } else if (match instanceof FileMatch) { - text = fileMatchToString(match, maxClipboardMatches).text; + text = fileMatchToString(match, maxClipboardMatches, labelService).text; } else if (match instanceof BaseFolderMatch) { - text = folderMatchToString(match, maxClipboardMatches).text; + text = folderMatchToString(match, maxClipboardMatches, labelService).text; } if (text) { @@ -753,12 +749,12 @@ export const copyMatchCommand: ICommandHandler = async (accessor, match: Rendera } }; -function allFolderMatchesToString(folderMatches: Array, maxMatches: number): string { +function allFolderMatchesToString(folderMatches: Array, maxMatches: number, labelService: ILabelService): string { const folderResults: string[] = []; let numMatches = 0; folderMatches = folderMatches.sort(searchMatchComparer); for (let i = 0; i < folderMatches.length && numMatches < maxMatches; i++) { - const folderResult = folderMatchToString(folderMatches[i], maxMatches - numMatches); + const folderResult = folderMatchToString(folderMatches[i], maxMatches - numMatches, labelService); if (folderResult.count) { numMatches += folderResult.count; folderResults.push(folderResult.text); @@ -772,12 +768,13 @@ export const copyAllCommand: ICommandHandler = async (accessor) => { const viewletService = accessor.get(IViewletService); const panelService = accessor.get(IPanelService); const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); const searchView = getSearchView(viewletService, panelService); if (searchView) { const root = searchView.searchResult; - const text = allFolderMatchesToString(root.folderMatches(), maxClipboardMatches); + const text = allFolderMatchesToString(root.folderMatches(), maxClipboardMatches, labelService); await clipboardService.writeText(text); } }; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 2daad95ca686e..33e16e9f22d0f 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -61,6 +61,7 @@ import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/v import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -153,6 +154,7 @@ export class SearchView extends ViewletPanel { @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, + @IOpenerService private readonly openerService: IOpenerService ) { super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService); @@ -364,6 +366,7 @@ export class SearchView extends ViewletPanel { const searchHistory = history.search || this.viewletState['query.searchHistory'] || []; const replaceHistory = history.replace || this.viewletState['query.replaceHistory'] || []; const showReplace = typeof this.viewletState['view.showReplace'] === 'boolean' ? this.viewletState['view.showReplace'] : true; + const preserveCase = this.viewletState['query.preserveCase'] === true; this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, { value: contentPattern, @@ -372,7 +375,8 @@ export class SearchView extends ViewletPanel { isCaseSensitive: isCaseSensitive, isWholeWords: isWholeWords, searchHistory: searchHistory, - replaceHistory: replaceHistory + replaceHistory: replaceHistory, + preserveCase: preserveCase })); if (showReplace) { @@ -390,6 +394,12 @@ export class SearchView extends ViewletPanel { this.viewModel.replaceActive = state; this.refreshTree(); })); + + this._register(this.searchWidget.onPreserveCaseChange((state) => { + this.viewModel.preserveCase = state; + this.refreshTree(); + })); + this._register(this.searchWidget.onReplaceValueChanged((value) => { this.viewModel.replaceString = this.searchWidget.getReplaceValue(); this.delayedRefresh.trigger(() => this.refreshTree()); @@ -1455,7 +1465,7 @@ export class SearchView extends ViewletPanel { this.openSettings('.exclude'); } - private openSettings(query: string): Promise { + private openSettings(query: string): Promise { const options: ISettingsEditorOptions = { query }; return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? this.preferencesService.openWorkspaceSettings(undefined, options) : @@ -1465,7 +1475,7 @@ export class SearchView extends ViewletPanel { private onLearnMore = (e: MouseEvent): void => { dom.EventHelper.stop(e, false); - window.open('https://go.microsoft.com/fwlink/?linkid=853977'); + this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=853977')); } private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void { @@ -1641,6 +1651,7 @@ export class SearchView extends ViewletPanel { const patternExcludes = this.inputPatternExcludes.getValue().trim(); const patternIncludes = this.inputPatternIncludes.getValue().trim(); const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); + const preserveCase = this.viewModel.preserveCase; this.viewletState['query.contentPattern'] = contentPattern; this.viewletState['query.regex'] = isRegex; @@ -1649,6 +1660,7 @@ export class SearchView extends ViewletPanel { this.viewletState['query.folderExclusions'] = patternExcludes; this.viewletState['query.folderIncludes'] = patternIncludes; this.viewletState['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles; + this.viewletState['query.preserveCase'] = preserveCase; const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown(); this.viewletState['view.showReplace'] = isReplaceShown; diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 6cfbb4d554209..26b29b280c182 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -33,6 +33,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; +import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; export interface ISearchWidgetOptions { value?: string; @@ -42,6 +43,7 @@ export interface ISearchWidgetOptions { isWholeWords?: boolean; searchHistory?: string[]; replaceHistory?: string[]; + preserveCase?: boolean; } class ReplaceAllAction extends Action { @@ -97,6 +99,7 @@ export class SearchWidget extends Widget { replaceInputFocusTracker: dom.IFocusTracker; private replaceInputBoxFocused: IContextKey; private _replaceHistoryDelayer: Delayer; + private _preserveCase: Checkbox; private ignoreGlobalFindBufferOnNextFocus = false; private previousGlobalFindBufferValue: string; @@ -113,6 +116,9 @@ export class SearchWidget extends Widget { private _onReplaceStateChange = this._register(new Emitter()); readonly onReplaceStateChange: Event = this._onReplaceStateChange.event; + private _onPreserveCaseChange = this._register(new Emitter()); + readonly onPreserveCaseChange: Event = this._onPreserveCaseChange.event; + private _onReplaceValueChanged = this._register(new Emitter()); readonly onReplaceValueChanged: Event = this._onReplaceValueChanged.event; @@ -333,13 +339,34 @@ export class SearchWidget extends Widget { private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void { this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled')); - const replaceBox = dom.append(this.replaceContainer, dom.$('.input-box')); + const replaceBox = dom.append(this.replaceContainer, dom.$('.replace-input')); + this.replaceInput = this._register(new ContextScopedHistoryInputBox(replaceBox, this.contextViewService, { ariaLabel: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'), placeholder: nls.localize('search.replace.placeHolder', "Replace"), history: options.replaceHistory || [], flexibleHeight: true }, this.contextKeyService)); + + this._preserveCase = this._register(new Checkbox({ + actionClassName: 'monaco-preserve-case', + title: nls.localize('label.preserveCaseCheckbox', "Preserve Case"), + isChecked: !!options.preserveCase, + })); + + this._register(this._preserveCase.onChange(viaKeyboard => { + if (!viaKeyboard) { + this.replaceInput.focus(); + this._onPreserveCaseChange.fire(this._preserveCase.checked); + } + })); + + let controls = document.createElement('div'); + controls.className = 'controls'; + controls.style.display = 'block'; + controls.appendChild(this._preserveCase.domNode); + replaceBox.appendChild(controls); + this._register(attachInputBoxStyler(this.replaceInput, this.themeService)); this.onkeydown(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent)); this.replaceInput.value = options.replaceValue || ''; diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 0f87a2f56f675..10e3f0938343a 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -6,7 +6,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; -import { SymbolKind, Location, ProviderResult } from 'vs/editor/common/modes'; +import { SymbolKind, Location, ProviderResult, SymbolTag } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; @@ -19,6 +19,7 @@ export interface IWorkspaceSymbol { name: string; containerName?: string; kind: SymbolKind; + tags?: SymbolTag[]; location: Location; } diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 1c93ff5221ca2..1962709126f90 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -103,17 +103,17 @@ export class Match { } const fullMatchText = this.fullMatchText(); - let replaceString = searchModel.replacePattern.getReplaceString(fullMatchText); + let replaceString = searchModel.replacePattern.getReplaceString(fullMatchText, searchModel.preserveCase); // If match string is not matching then regex pattern has a lookahead expression if (replaceString === null) { const fullMatchTextWithTrailingContent = this.fullMatchText(true); - replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithTrailingContent); + replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithTrailingContent, searchModel.preserveCase); // Search/find normalize line endings - check whether \r prevents regex from matching if (replaceString === null) { const fullMatchTextWithoutCR = fullMatchTextWithTrailingContent.replace(/\r\n/g, '\n'); - replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithoutCR); + replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithoutCR, searchModel.preserveCase); } } @@ -895,6 +895,7 @@ export class SearchModel extends Disposable { private _replaceActive: boolean = false; private _replaceString: string | null = null; private _replacePattern: ReplacePattern | null = null; + private _preserveCase: boolean = false; private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; @@ -926,6 +927,14 @@ export class SearchModel extends Disposable { return this._replaceString || ''; } + set preserveCase(value: boolean) { + this._preserveCase = value; + } + + get preserveCase(): boolean { + return this._preserveCase; + } + set replaceString(replaceString: string) { this._replaceString = replaceString; if (this._searchQuery) { diff --git a/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts index 0c14fd010f700..ed8fd45887718 100644 --- a/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts @@ -14,6 +14,7 @@ import { IFolderQuery, IPatternInfo, QueryType, ITextQuery, IFileQuery } from 'v import { IWorkspaceContextService, toWorkspaceFolder, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { ISearchPathsInfo, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { TestContextService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { isWindows } from 'vs/base/common/platform'; const DEFAULT_EDITOR_CONFIG = {}; const DEFAULT_USER_CONFIG = { useRipgrep: true, useIgnoreFiles: true, useGlobalIgnoreFiles: true }; @@ -1032,7 +1033,7 @@ function getUri(...slashPathParts: string[]): uri { } function fixPath(...slashPathParts: string[]): string { - if (process.platform === 'win32' && slashPathParts.length && !slashPathParts[0].match(/^c:/i)) { + if (isWindows && slashPathParts.length && !slashPathParts[0].match(/^c:/i)) { slashPathParts.unshift('c:'); } diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index ba00d3b20a13f..7ad9e679d1f2b 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -18,6 +18,7 @@ import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchPro import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; +import * as process from 'vs/base/common/process'; const nullEvent = new class { id: number; diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index 5c84fba277593..e15578264ec55 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -168,13 +168,14 @@ class InsertSnippetAction extends EditorAction { return quickInputService.pick(picks, { matchOnDetail: true }).then(pick => resolve(pick && pick.snippet), reject); } }).then(async snippet => { + if (!snippet) { + return; + } let clipboardText: string | undefined; if (snippet.needsClipboard) { clipboardText = await clipboardService.readText(); } - if (snippet) { - SnippetController2.get(editor).insert(snippet.codeSnippet, { clipboardText }); - } + SnippetController2.get(editor).insert(snippet.codeSnippet, { clipboardText }); }); } } diff --git a/src/vs/workbench/contrib/stats/common/workspaceStats.ts b/src/vs/workbench/contrib/stats/common/workspaceStats.ts new file mode 100644 index 0000000000000..7bd69fc83cda9 --- /dev/null +++ b/src/vs/workbench/contrib/stats/common/workspaceStats.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; + +export type Tags = { [index: string]: boolean | number | string | undefined }; + +export const IWorkspaceStatsService = createDecorator('workspaceStatsService'); + +export interface IWorkspaceStatsService { + _serviceBrand: any; + + getTags(): Promise; + + /** + * Returns an id for the workspace, different from the id returned by the context service. A hash based + * on the folder uri or workspace configuration, not time-based, and undefined for empty workspaces. + */ + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined; + + getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise; +} diff --git a/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts b/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts index 89baf8b21e451..e6ca4e0869bad 100644 --- a/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts @@ -14,7 +14,8 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { endsWith } from 'vs/base/common/strings'; import { ITextFileService, } from 'vs/workbench/services/textfile/common/textfiles'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; +import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/common/workspaceStats'; +import { IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/; const SshUrlMatcher = /^([^@:]+@)?([^:]+):(.+)$/; @@ -135,20 +136,6 @@ export function getHashedRemotesFromConfig(text: string, stripEndingDotGit: bool }); } -export function getHashedRemotesFromUri(workspaceUri: URI, fileService: IFileService, textFileService: ITextFileService, stripEndingDotGit: boolean = false): Promise { - const path = workspaceUri.path; - const uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` }); - return fileService.exists(uri).then(exists => { - if (!exists) { - return []; - } - return textFileService.read(uri, { acceptTextOnly: true }).then( - content => getHashedRemotesFromConfig(content.value, stripEndingDotGit), - err => [] // ignore missing or binary file - ); - }); -} - export class WorkspaceStats implements IWorkbenchContribution { constructor( @@ -160,7 +147,9 @@ export class WorkspaceStats implements IWorkbenchContribution { @ISharedProcessService private readonly sharedProcessService: ISharedProcessService, @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService ) { - this.report(); + if (this.telemetryService.isOptedIn) { + this.report(); + } } private report(): void { @@ -175,10 +164,20 @@ export class WorkspaceStats implements IWorkbenchContribution { this.reportProxyStats(); const diagnosticsChannel = this.sharedProcessService.getChannel('diagnostics'); - diagnosticsChannel.call('reportWorkspaceStats', this.contextService.getWorkspace()); + diagnosticsChannel.call('reportWorkspaceStats', this.getWorkspaceInformation()); } - + private getWorkspaceInformation(): IWorkspaceInformation { + const workspace = this.contextService.getWorkspace(); + const state = this.contextService.getWorkbenchState(); + const id = this.workspaceStatsService.getTelemetryWorkspaceId(workspace, state); + return { + id: workspace.id, + telemetryId: id, + folders: workspace.folders, + configuration: workspace.configuration + }; + } private reportWorkspaceTags(tags: Tags): void { /* __GDPR__ @@ -219,7 +218,7 @@ export class WorkspaceStats implements IWorkbenchContribution { private reportRemotes(workspaceUris: URI[]): void { Promise.all(workspaceUris.map(workspaceUri => { - return getHashedRemotesFromUri(workspaceUri, this.fileService, this.textFileService, true); + return this.workspaceStatsService.getHashedRemotesFromUri(workspaceUri, true); })).then(hashedRemotes => { /* __GDPR__ "workspace.hashedRemotes" : { diff --git a/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts b/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts index 9d0e84928f00b..2cb249cba4761 100644 --- a/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts +++ b/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts @@ -5,12 +5,11 @@ import * as crypto from 'crypto'; import { IFileService, IResolveFileResult, IFileStat } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWindowService, IWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { INotificationService, NeverShowAgainScope, INeverShowAgainOptions } from 'vs/platform/notification/common/notification'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITextFileService, ITextFileContent } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -18,12 +17,9 @@ import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspa import { localize } from 'vs/nls'; import Severity from 'vs/base/common/severity'; import { joinPath } from 'vs/base/common/resources'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export type Tags = { [index: string]: boolean | number | string | undefined }; - -const DISABLE_WORKSPACE_PROMPT_KEY = 'workspaces.dontPromptToOpen'; +import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/common/workspaceStats'; +import { getHashedRemotesFromConfig } from 'vs/workbench/contrib/stats/electron-browser/workspaceStats'; const ModulesToLookFor = [ // Packages that suggest a node server @@ -93,14 +89,6 @@ const PyModulesToLookFor = [ 'botframework-connector' ]; -export const IWorkspaceStatsService = createDecorator('workspaceStatsService'); - -export interface IWorkspaceStatsService { - _serviceBrand: any; - getTags(): Promise; -} - - export class WorkspaceStatsService implements IWorkspaceStatsService { _serviceBrand: any; private _tags: Tags; @@ -112,7 +100,6 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { @IWindowService private readonly windowService: IWindowService, @INotificationService private readonly notificationService: INotificationService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IStorageService private readonly storageService: IStorageService, @ITextFileService private readonly textFileService: ITextFileService ) { } @@ -124,6 +111,42 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { return this._tags; } + public getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined { + function createHash(uri: URI): string { + return crypto.createHash('sha1').update(uri.scheme === Schemas.file ? uri.fsPath : uri.toString()).digest('hex'); + } + + let workspaceId: string | undefined; + switch (state) { + case WorkbenchState.EMPTY: + workspaceId = undefined; + break; + case WorkbenchState.FOLDER: + workspaceId = createHash(workspace.folders[0].uri); + break; + case WorkbenchState.WORKSPACE: + if (workspace.configuration) { + workspaceId = createHash(workspace.configuration); + } + } + + return workspaceId; + } + + getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit: boolean = false): Promise { + const path = workspaceUri.path; + const uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` }); + return this.fileService.exists(uri).then(exists => { + if (!exists) { + return []; + } + return this.textFileService.read(uri, { acceptTextOnly: true }).then( + content => getHashedRemotesFromConfig(content.value, stripEndingDotGit), + err => [] // ignore missing or binary file + ); + }); + } + /* __GDPR__FRAGMENT__ "WorkspaceTags" : { "workbench.filesToOpenOrCreate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -226,25 +249,7 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { const state = this.contextService.getWorkbenchState(); const workspace = this.contextService.getWorkspace(); - function createHash(uri: URI): string { - return crypto.createHash('sha1').update(uri.scheme === Schemas.file ? uri.fsPath : uri.toString()).digest('hex'); - } - - let workspaceId: string | undefined; - switch (state) { - case WorkbenchState.EMPTY: - workspaceId = undefined; - break; - case WorkbenchState.FOLDER: - workspaceId = createHash(workspace.folders[0].uri); - break; - case WorkbenchState.WORKSPACE: - if (workspace.configuration) { - workspaceId = createHash(workspace.configuration); - } - } - - tags['workspace.id'] = workspaceId; + tags['workspace.id'] = this.getTelemetryWorkspaceId(workspace, state); const { filesToOpenOrCreate, filesToDiff } = configuration; tags['workbench.filesToOpenOrCreate'] = filesToOpenOrCreate && filesToOpenOrCreate.length || 0; @@ -440,15 +445,7 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { } private doHandleWorkspaceFiles(folder: URI, workspaces: string[]): void { - if (this.storageService.getBoolean(DISABLE_WORKSPACE_PROMPT_KEY, StorageScope.WORKSPACE)) { - return; // prompt disabled by user - } - - const doNotShowAgain: IPromptChoice = { - label: localize('never again', "Don't Show Again"), - isSecondary: true, - run: () => this.storageService.store(DISABLE_WORKSPACE_PROMPT_KEY, true, StorageScope.WORKSPACE) - }; + const neverShowAgain: INeverShowAgainOptions = { id: 'workspaces.dontPromptToOpen', scope: NeverShowAgainScope.WORKSPACE, isSecondary: true }; // Prompt to open one workspace if (workspaces.length === 1) { @@ -457,7 +454,7 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { this.notificationService.prompt(Severity.Info, localize('workspaceFound', "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", workspaceFile, 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ label: localize('openWorkspace', "Open Workspace"), run: () => this.windowService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }]) - }, doNotShowAgain]); + }], { neverShowAgain }); } // Prompt to select a workspace from many @@ -473,7 +470,7 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { } }); } - }, doNotShowAgain]); + }], { neverShowAgain }); } } diff --git a/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts index ec9ac3fb2b770..44cf53887ff37 100644 --- a/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/electron-browser/languageSurveys.contribution.ts @@ -16,6 +16,8 @@ import { ISurveyData } from 'vs/platform/product/common/product'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; class LanguageSurvey { @@ -25,7 +27,8 @@ class LanguageSurvey { notificationService: INotificationService, telemetryService: ITelemetryService, modelService: IModelService, - textFileService: ITextFileService + textFileService: ITextFileService, + openerService: IOpenerService ) { const SESSION_COUNT_KEY = `${data.surveyId}.sessionCount`; const LAST_SESSION_DATE_KEY = `${data.surveyId}.lastSessionDate`; @@ -94,7 +97,7 @@ class LanguageSurvey { run: () => { telemetryService.publicLog(`${data.surveyId}.survey/takeShortSurvey`); telemetryService.getTelemetryInfo().then(info => { - window.open(`${data.surveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`); + openerService.open(URI.parse(`${data.surveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`)); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); }); @@ -126,11 +129,12 @@ class LanguageSurveysContribution implements IWorkbenchContribution { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IModelService modelService: IModelService, - @ITextFileService textFileService: ITextFileService + @ITextFileService textFileService: ITextFileService, + @IOpenerService openerService: IOpenerService ) { product.surveys .filter(surveyData => surveyData.surveyId && surveyData.editCount && surveyData.languageId && surveyData.surveyUrl && surveyData.userProbability) - .map(surveyData => new LanguageSurvey(surveyData, storageService, notificationService, telemetryService, modelService, textFileService)); + .map(surveyData => new LanguageSurvey(surveyData, storageService, notificationService, telemetryService, modelService, textFileService, openerService)); } } diff --git a/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts index c11fd38db201c..5e4433ce2ad9c 100644 --- a/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/electron-browser/nps.contribution.ts @@ -13,6 +13,8 @@ import pkg from 'vs/platform/product/node/package'; import product from 'vs/platform/product/node/product'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; const PROBABILITY = 0.15; const SESSION_COUNT_KEY = 'nps/sessionCount'; @@ -25,7 +27,8 @@ class NPSContribution implements IWorkbenchContribution { constructor( @IStorageService storageService: IStorageService, @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IOpenerService openerService: IOpenerService ) { const skipVersion = storageService.get(SKIP_VERSION_KEY, StorageScope.GLOBAL, ''); if (skipVersion) { @@ -64,7 +67,7 @@ class NPSContribution implements IWorkbenchContribution { label: nls.localize('takeSurvey', "Take Survey"), run: () => { telemetryService.getTelemetryInfo().then(info => { - window.open(`${product.npsSurveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`); + openerService.open(URI.parse(`${product.npsSurveyUrl}?o=${encodeURIComponent(process.platform)}&v=${encodeURIComponent(pkg.version)}&m=${encodeURIComponent(info.machineId)}`)); storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL); storageService.store(SKIP_VERSION_KEY, pkg.version, StorageScope.GLOBAL); }); @@ -87,4 +90,4 @@ class NPSContribution implements IWorkbenchContribution { if (language === 'en' && product.npsSurveyUrl) { const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(NPSContribution, LifecyclePhase.Restored); -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 5451b8eb6f4b9..461797d862c1b 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -179,10 +179,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private static nextHandle: number = 0; - private _schemaVersion: JsonSchemaVersion; - private _executionEngine: ExecutionEngine; - private _workspaceFolders: IWorkspaceFolder[]; - private _ignoredWorkspaceFolders: IWorkspaceFolder[]; + private _schemaVersion: JsonSchemaVersion | undefined; + private _executionEngine: ExecutionEngine | undefined; + private _workspaceFolders: IWorkspaceFolder[] | undefined; + private _ignoredWorkspaceFolders: IWorkspaceFolder[] | undefined; private _showIgnoreMessage?: boolean; private _providers: Map; private _providerTypes: Map; @@ -192,7 +192,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected _taskSystem?: ITaskSystem; protected _taskSystemListener?: IDisposable; - private _recentlyUsedTasks: LinkedMap; + private _recentlyUsedTasks: LinkedMap | undefined; protected _taskRunningState: IContextKey; @@ -375,28 +375,28 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!this._workspaceFolders) { this.updateSetup(); } - return this._workspaceFolders; + return this._workspaceFolders!; } private get ignoredWorkspaceFolders(): IWorkspaceFolder[] { if (!this._ignoredWorkspaceFolders) { this.updateSetup(); } - return this._ignoredWorkspaceFolders; + return this._ignoredWorkspaceFolders!; } protected get executionEngine(): ExecutionEngine { if (this._executionEngine === undefined) { this.updateSetup(); } - return this._executionEngine; + return this._executionEngine!; } private get schemaVersion(): JsonSchemaVersion { if (this._schemaVersion === undefined) { this.updateSetup(); } - return this._schemaVersion; + return this._schemaVersion!; } private get showIgnoreMessage(): boolean { @@ -1620,7 +1620,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer nls.localize('TaskService.noWorkspace', "Tasks are only available on a workspace folder."), [{ label: nls.localize('TaskService.learnMore', "Learn More"), - run: () => window.open('https://code.visualstudio.com/docs/editor/tasks') + run: () => this.openerService.open(URI.parse('https://code.visualstudio.com/docs/editor/tasks')) }] ); return false; diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index b3bccaf94e007..cbe96131cb7e3 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -12,6 +12,7 @@ import { RunOnOptions, Task, TaskRunSource } from 'vs/workbench/contrib/tasks/co import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Action } from 'vs/base/common/actions'; +import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; @@ -131,38 +132,27 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut } -export class AllowAutomaticTaskRunning extends Action { +export class ManageAutomaticTaskRunning extends Action { - public static readonly ID = 'workbench.action.tasks.allowAutomaticRunning'; - public static readonly LABEL = nls.localize('workbench.action.tasks.allowAutomaticRunning', "Allow Automatic Tasks in Folder"); + public static readonly ID = 'workbench.action.tasks.manageAutomaticRunning'; + public static readonly LABEL = nls.localize('workbench.action.tasks.manageAutomaticRunning', "Manage Automatic Tasks in Folder"); constructor( id: string, label: string, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(id, label); } - public run(event?: any): Promise { - this.storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, true, StorageScope.WORKSPACE); - return Promise.resolve(undefined); - } -} - -export class DisallowAutomaticTaskRunning extends Action { - - public static readonly ID = 'workbench.action.tasks.disallowAutomaticRunning'; - public static readonly LABEL = nls.localize('workbench.action.tasks.disallowAutomaticRunning', "Disallow Automatic Tasks in Folder"); - - constructor( - id: string, label: string, - @IStorageService private readonly storageService: IStorageService - ) { - super(id, label); - } + public async run(event?: any): Promise { + const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.allowAutomaticTasks', "Allow Automatic Tasks in Folder") }; + const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.disallowAutomaticTasks', "Disallow Automatic Tasks in Folder") }; + const value = await this.quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); + if (!value) { + return; + } - public run(event?: any): Promise { - this.storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, false, StorageScope.WORKSPACE); - return Promise.resolve(undefined); + this.storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, value === allowItem, StorageScope.WORKSPACE); } } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index de6fbc81085e6..92d2f493108c9 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -32,7 +32,7 @@ import { QuickOpenActionContributor } from '../browser/quickOpen'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { RunAutomaticTasks, AllowAutomaticTaskRunning, DisallowAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; +import { RunAutomaticTasks, ManageAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; let tasksCategory = nls.localize('tasksCategory', "Tasks"); @@ -40,8 +40,7 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(RunAutomaticTasks, LifecyclePhase.Eventually); const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(AllowAutomaticTaskRunning, AllowAutomaticTaskRunning.ID, AllowAutomaticTaskRunning.LABEL), 'Tasks: Allow Automatic Tasks in Folder', tasksCategory); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DisallowAutomaticTaskRunning, DisallowAutomaticTaskRunning.ID, DisallowAutomaticTaskRunning.LABEL), 'Tasks: Disallow Automatic Tasks in Folder', tasksCategory); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ManageAutomaticTaskRunning, ManageAutomaticTaskRunning.ID, ManageAutomaticTaskRunning.LABEL), 'Tasks: Manage Automatic Tasks in Folder', tasksCategory); export class TaskStatusBarContributions extends Disposable implements IWorkbenchContribution { private runningTasksStatusItem: IStatusbarEntryAccessor | undefined; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 9d52970cf2cd5..d56d5f1c70516 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -44,6 +44,7 @@ import { Schemas } from 'vs/base/common/network'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { env as processEnv, cwd as processCwd } from 'vs/base/common/process'; interface TerminalData { terminal: ITerminalInstance; @@ -155,9 +156,10 @@ export class TerminalTaskSystem implements ITaskSystem { private idleTaskTerminals: LinkedMap; private sameTaskTerminals: IStringDictionary; private taskSystemInfoResolver: TaskSystemInfoResolver; - private lastTask: VerifiedTask; - private currentTask: VerifiedTask; - private isRerun: boolean; + private lastTask: VerifiedTask | undefined; + // Should always be set in run + private currentTask!: VerifiedTask; + private isRerun: boolean = false; private readonly _onDidStateChange: Emitter; @@ -338,8 +340,7 @@ export class TerminalTaskSystem implements ITaskSystem { private async executeTask(task: Task, resolver: ITaskResolver, trigger: string): Promise { let promises: Promise[] = []; if (task.configurationProperties.dependsOn) { - for (let index in task.configurationProperties.dependsOn) { - const dependency = task.configurationProperties.dependsOn[index]; + for (const dependency of task.configurationProperties.dependsOn) { let dependencyTask = resolver.resolve(dependency.workspaceFolder, dependency.task!); if (dependencyTask) { let key = dependencyTask.getMapKey(); @@ -485,28 +486,32 @@ export class TerminalTaskSystem implements ITaskSystem { } private reexecuteCommand(task: CustomTask | ContributedTask, trigger: string): Promise { - const workspaceFolder = this.currentTask.workspaceFolder = this.lastTask.workspaceFolder; + const lastTask = this.lastTask; + if (!lastTask) { + return Promise.reject(new Error('No task previously run')); + } + const workspaceFolder = this.currentTask.workspaceFolder = lastTask.workspaceFolder; let variables = new Set(); this.collectTaskVariables(variables, task); // Check that the task hasn't changed to include new variables let hasAllVariables = true; variables.forEach(value => { - if (value.substring(2, value.length - 1) in this.lastTask.getVerifiedTask().resolvedVariables) { + if (value.substring(2, value.length - 1) in lastTask.getVerifiedTask().resolvedVariables) { hasAllVariables = false; } }); if (!hasAllVariables) { - return this.resolveVariablesFromSet(this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => { + return this.resolveVariablesFromSet(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => { this.currentTask.resolvedVariables = resolvedVariables; - return this.executeInTerminal(task, trigger, new VariableResolver(this.lastTask.getVerifiedTask().workspaceFolder, this.lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); + return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); }, reason => { return Promise.reject(reason); }); } else { - this.currentTask.resolvedVariables = this.lastTask.getVerifiedTask().resolvedVariables; - return this.executeInTerminal(task, trigger, new VariableResolver(this.lastTask.getVerifiedTask().workspaceFolder, this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); + this.currentTask.resolvedVariables = lastTask.getVerifiedTask().resolvedVariables; + return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); } } @@ -923,7 +928,7 @@ export class TerminalTaskSystem implements ITaskSystem { args = resolvedResult.args; commandExecutable = CommandString.value(command); - this.currentTask.shellLaunchConfig = launchConfigs = this.isRerun ? this.lastTask.getVerifiedTask().shellLaunchConfig : await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); + this.currentTask.shellLaunchConfig = launchConfigs = (this.isRerun && this.lastTask) ? this.lastTask.getVerifiedTask().shellLaunchConfig : await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); if (launchConfigs === undefined) { return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; } @@ -1370,7 +1375,7 @@ export class TerminalTaskSystem implements ITaskSystem { return command; } if (cwd === undefined) { - cwd = process.cwd(); + cwd = processCwd(); } const dir = path.dirname(command); if (dir !== '.') { @@ -1378,8 +1383,8 @@ export class TerminalTaskSystem implements ITaskSystem { // to the current working directory. return path.join(cwd, command); } - if (paths === undefined && Types.isString(process.env.PATH)) { - paths = process.env.PATH.split(path.delimiter); + if (paths === undefined && Types.isString(processEnv.PATH)) { + paths = processEnv.PATH.split(path.delimiter); } // No PATH environment. Make path absolute to the cwd. if (paths === undefined || paths.length === 0) { diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index eac610b4d4af0..30333ecdc3df5 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -44,10 +44,10 @@ export abstract class AbstractProblemCollector implements IDisposable { private bufferLength: number; private openModels: IStringDictionary; private readonly modelListeners = new DisposableStore(); - private tail: Promise; + private tail: Promise | undefined; // [owner] -> ApplyToKind - private applyToByOwner: Map; + protected applyToByOwner: Map; // [owner] -> [resource] -> URI private resourcesToClean: Map>; // [owner] -> [resource] -> [markerkey] -> markerData @@ -278,9 +278,14 @@ export abstract class AbstractProblemCollector implements IDisposable { markersPerResource = new Map(); markersPerOwner.set(resourceAsString, markersPerResource); } - let key = IMarkerData.makeKey(marker); + let key = IMarkerData.makeKeyOptionalMessage(marker, false); + let existingMarker; if (!markersPerResource.has(key)) { markersPerResource.set(key, marker); + } else if (((existingMarker = markersPerResource.get(key)) !== undefined) && existingMarker.message.length < marker.message.length) { + // Most likely https://github.com/microsoft/vscode/issues/77475 + // Heuristic dictates that when the key is the same and message is smaller, we have hit this limitation. + markersPerResource.set(key, marker); } } @@ -344,8 +349,8 @@ export const enum ProblemHandlingStrategy { export class StartStopProblemCollector extends AbstractProblemCollector implements IProblemMatcher { private owners: string[]; - private currentOwner: string; - private currentResource: string; + private currentOwner: string | undefined; + private currentResource: string | undefined; constructor(problemMatchers: ProblemMatcher[], markerService: IMarkerService, modelService: IModelService, _strategy: ProblemHandlingStrategy = ProblemHandlingStrategy.Clean, fileService?: IFileService) { super(problemMatchers, markerService, modelService, fileService); @@ -397,8 +402,8 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement private _activeBackgroundMatchers: Set; // Current State - private currentOwner: string | null; - private currentResource: string | null; + private currentOwner: string | undefined; + private currentResource: string | undefined; constructor(problemMatchers: ProblemMatcher[], markerService: IMarkerService, modelService: IModelService, fileService?: IFileService) { super(problemMatchers, markerService, modelService, fileService); @@ -503,8 +508,8 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement private resetCurrentResource(): void { this.reportMarkersForCurrentResource(); - this.currentOwner = null; - this.currentResource = null; + this.currentOwner = undefined; + this.currentResource = undefined; } private reportMarkersForCurrentResource(): void { @@ -512,4 +517,11 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement this.deliverMarkersPerOwnerAndResource(this.currentOwner, this.currentResource); } } -} \ No newline at end of file + + public done(): void { + [...this.applyToByOwner.keys()].forEach(owner => { + this.recordResourcesToClean(owner); + }); + super.done(); + } +} diff --git a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts index 18b98f751f0c8..73f8f8f218f16 100644 --- a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts @@ -216,11 +216,11 @@ export async function getResource(filename: string, matcher: ProblemMatcher, fil if (fullPath === undefined) { throw new Error('FileLocationKind is not actionable. Does the matcher have a filePrefix? This should never happen.'); } + fullPath = normalize(fullPath); fullPath = fullPath.replace(/\\/g, '/'); if (fullPath[0] !== '/') { fullPath = '/' + fullPath; } - fullPath = normalize(fullPath); if (matcher.uriProvider !== undefined) { return matcher.uriProvider(fullPath); } else { @@ -264,7 +264,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { public abstract get matchLength(): number; - protected fillProblemData(data: ProblemData | null, pattern: ProblemPattern, matches: RegExpExecArray): data is ProblemData { + protected fillProblemData(data: ProblemData | undefined, pattern: ProblemPattern, matches: RegExpExecArray): data is ProblemData { if (data) { this.fillProperty(data, 'file', pattern, matches, true); this.appendProperty(data, 'message', pattern, matches, true); @@ -449,7 +449,7 @@ class SingleLineMatcher extends AbstractLineMatcher { class MultiLineMatcher extends AbstractLineMatcher { private patterns: ProblemPattern[]; - private data: ProblemData | null; + private data: ProblemData | undefined; constructor(matcher: ProblemMatcher, fileService?: IFileService) { super(matcher, fileService); @@ -480,7 +480,7 @@ class MultiLineMatcher extends AbstractLineMatcher { } let loop = !!this.patterns[this.patterns.length - 1].loop; if (!loop) { - this.data = null; + this.data = undefined; } const markerMatch = data ? this.getMarkerMatch(data) : null; return { match: markerMatch ? markerMatch : null, continue: loop }; @@ -491,7 +491,7 @@ class MultiLineMatcher extends AbstractLineMatcher { Assert.ok(pattern.loop === true && this.data !== null); let matches = pattern.regexp.exec(line); if (!matches) { - this.data = null; + this.data = undefined; return null; } let data = Objects.deepClone(this.data); @@ -794,7 +794,7 @@ export namespace Config { fileLocation?: string | string[]; /** - * The name of a predefined problem pattern, the inline definintion + * The name of a predefined problem pattern, the inline definition * of a problem pattern or an array of problem patterns to match * problems spread over multiple lines. */ diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 4e856c3b65c2b..d0e83e2e000f6 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -681,7 +681,7 @@ export namespace RunOptions { } } -class ParseContext { +interface ParseContext { workspaceFolder: IWorkspaceFolder; problemReporter: IProblemReporter; namedProblemMatchers: IStringDictionary; diff --git a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts index b52a02294869c..6a696c1666aa5 100644 --- a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts @@ -89,7 +89,7 @@ class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { private taskTypes: IStringDictionary; private readyPromise: Promise; - private _schema: IJSONSchema; + private _schema: IJSONSchema | undefined; constructor() { this.taskTypes = Object.create(null); diff --git a/src/vs/workbench/contrib/tasks/common/taskTemplates.ts b/src/vs/workbench/contrib/tasks/common/taskTemplates.ts index a0412d55268ce..c367ede9ab7da 100644 --- a/src/vs/workbench/contrib/tasks/common/taskTemplates.ts +++ b/src/vs/workbench/contrib/tasks/common/taskTemplates.ts @@ -27,9 +27,10 @@ const dotnetBuild: TaskEntry = { '\t"tasks": [', '\t\t{', '\t\t\t"label": "build",', - '\t\t\t"command": "dotnet build",', + '\t\t\t"command": "dotnet",', '\t\t\t"type": "shell",', '\t\t\t"args": [', + '\t\t\t\t"build",', '\t\t\t\t// Ask dotnet build to generate full paths for file names.', '\t\t\t\t"/property:GenerateFullPaths=true",', '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 46593df22bee6..24d77f427d789 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -518,7 +518,7 @@ export abstract class CommonTask { /** * The cached label. */ - _label: string; + _label: string = ''; type?: string; @@ -614,7 +614,7 @@ export abstract class CommonTask { export class CustomTask extends CommonTask { - type: '$customized'; // CUSTOMIZED_TASK_TYPE + type!: '$customized'; // CUSTOMIZED_TASK_TYPE /** * Indicated the source of the task (e.g. tasks.json or extension) @@ -626,7 +626,7 @@ export class CustomTask extends CommonTask { /** * The command configuration */ - command: CommandConfiguration; + command: CommandConfiguration = {}; public constructor(id: string, source: WorkspaceTaskSource, label: string, type: string, command: CommandConfiguration | undefined, hasDefinedMatchers: boolean, runOptions: RunOptions, configurationProperties: ConfigurationProperties) { @@ -754,8 +754,9 @@ export class ContributedTask extends CommonTask { /** * Indicated the source of the task (e.g. tasks.json or extension) + * Set in the super constructor */ - _source: ExtensionTaskSource; + _source!: ExtensionTaskSource; defines: KeyedTaskIdentifier; @@ -824,7 +825,7 @@ export class InMemoryTask extends CommonTask { */ _source: InMemoryTaskSource; - type: 'inMemory'; + type!: 'inMemory'; public constructor(id: string, source: InMemoryTaskSource, label: string, type: string, runOptions: RunOptions, configurationProperties: ConfigurationProperties) { @@ -984,14 +985,14 @@ export namespace KeyedTaskIdentifier { function sortedStringify(literal: any): string { const keys = Object.keys(literal).sort(); let result: string = ''; - for (let position in keys) { - let stringified = literal[keys[position]]; + for (const key of keys) { + let stringified = literal[key]; if (stringified instanceof Object) { stringified = sortedStringify(stringified); } else if (typeof stringified === 'string') { stringified = stringified.replace(/,/g, ',,'); } - result += keys[position] + ',' + stringified + ','; + result += key + ',' + stringified + ','; } return result; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index dbd0aa4128647..db587f13986e4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -20,7 +20,7 @@ import * as panel from 'vs/workbench/browser/panel'; import { getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { Extensions as QuickOpenExtensions, IQuickOpenRegistry, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { AllowWorkspaceShellTerminalCommand, ClearSelectionTerminalAction, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewInActiveWorkspaceTerminalAction, CreateNewTerminalAction, DeleteToLineStartTerminalAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, DisallowWorkspaceShellTerminalCommand, FindNext, FindPrevious, FocusActiveTerminalAction, FocusNextPaneTerminalAction, FocusNextTerminalAction, FocusPreviousPaneTerminalAction, FocusPreviousTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, KillTerminalAction, MoveToLineEndTerminalAction, MoveToLineStartTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, RenameTerminalAction, ResizePaneDownTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, RunActiveFileInTerminalAction, RunSelectedTextInTerminalAction, ScrollDownPageTerminalAction, ScrollDownTerminalAction, ScrollToBottomTerminalAction, ScrollToNextCommandAction, ScrollToPreviousCommandAction, ScrollToTopTerminalAction, ScrollUpPageTerminalAction, ScrollUpTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SelectToNextCommandAction, SelectToNextLineAction, SelectToPreviousCommandAction, SelectToPreviousLineAction, SendSequenceTerminalCommand, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, TERMINAL_PICKER_PREFIX, ToggleCaseSensitiveCommand, ToggleEscapeSequenceLoggingAction, ToggleRegexCommand, ToggleTerminalAction, ToggleWholeWordCommand, NavigationModeFocusPreviousTerminalAction, NavigationModeFocusNextTerminalAction, NavigationModeExitTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { ClearSelectionTerminalAction, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewInActiveWorkspaceTerminalAction, CreateNewTerminalAction, DeleteToLineStartTerminalAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, FindNext, FindPrevious, FocusActiveTerminalAction, FocusNextPaneTerminalAction, FocusNextTerminalAction, FocusPreviousPaneTerminalAction, FocusPreviousTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, KillTerminalAction, MoveToLineEndTerminalAction, MoveToLineStartTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, RenameTerminalAction, ResizePaneDownTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, RunActiveFileInTerminalAction, RunSelectedTextInTerminalAction, ScrollDownPageTerminalAction, ScrollDownTerminalAction, ScrollToBottomTerminalAction, ScrollToNextCommandAction, ScrollToPreviousCommandAction, ScrollToTopTerminalAction, ScrollUpPageTerminalAction, ScrollUpTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SelectToNextCommandAction, SelectToNextLineAction, SelectToPreviousCommandAction, SelectToPreviousLineAction, SendSequenceTerminalCommand, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, TERMINAL_PICKER_PREFIX, ToggleCaseSensitiveCommand, ToggleEscapeSequenceLoggingAction, ToggleRegexCommand, ToggleTerminalAction, ToggleWholeWordCommand, NavigationModeFocusPreviousTerminalAction, NavigationModeFocusNextTerminalAction, NavigationModeExitTerminalAction, ManageWorkspaceShellPermissionsTerminalCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalPanel } from 'vs/workbench/contrib/terminal/browser/terminalPanel'; import { TerminalPickerHandler } from 'vs/workbench/contrib/terminal/browser/terminalQuickOpen'; import { KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, ITerminalService, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -72,17 +72,17 @@ configurationRegistry.registerConfiguration({ type: 'object', properties: { 'terminal.integrated.automationShell.linux': { - markdownDescription: nls.localize('terminal.integrated.automationShell.linux', "A path that when set will override {0} and ignore {1} and {2} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`', '`env`'), + markdownDescription: nls.localize('terminal.integrated.automationShell.linux', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`'), type: ['string', 'null'], default: null }, 'terminal.integrated.automationShell.osx': { - markdownDescription: nls.localize('terminal.integrated.automationShell.osx', "A path that when set will override {0} and ignore {1} and {2} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`', '`env`'), + markdownDescription: nls.localize('terminal.integrated.automationShell.osx', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`'), type: ['string', 'null'], default: null }, 'terminal.integrated.automationShell.windows': { - markdownDescription: nls.localize('terminal.integrated.automationShell.windows', "A path that when set will override {0} and ignore {1} and {2} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`', '`env`'), + markdownDescription: nls.localize('terminal.integrated.automationShell.windows', "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`'), type: ['string', 'null'], default: null }, @@ -298,6 +298,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."), type: 'boolean', default: false + }, + 'terminal.integrated.experimentalUseTitleEvent': { + description: nls.localize('terminal.integrated.experimentalUseTitleEvent', "An experimental setting that will use the terminal title event for the dropdown title. This setting will only apply to new terminals."), + type: 'boolean', + default: false } } }); @@ -387,8 +392,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearTerminalAct mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectDefaultShellWindowsTerminalAction, SelectDefaultShellWindowsTerminalAction.ID, SelectDefaultShellWindowsTerminalAction.LABEL), 'Terminal: Select Default Shell', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(AllowWorkspaceShellTerminalCommand, AllowWorkspaceShellTerminalCommand.ID, AllowWorkspaceShellTerminalCommand.LABEL), 'Terminal: Allow Workspace Shell Configuration', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DisallowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand.ID, DisallowWorkspaceShellTerminalCommand.LABEL), 'Terminal: Disallow Workspace Shell Configuration', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ManageWorkspaceShellPermissionsTerminalCommand, ManageWorkspaceShellPermissionsTerminalCommand.ID, ManageWorkspaceShellPermissionsTerminalCommand.LABEL), 'Terminal: Manage Workspace Shell Permissions', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(RenameTerminalAction, RenameTerminalAction.ID, RenameTerminalAction.LABEL), 'Terminal: Rename', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalFindWidgetAction, FocusTerminalFindWidgetAction.ID, FocusTerminalFindWidgetAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_F @@ -526,7 +530,8 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindNext, FindNe }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find next', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindNext, FindNext.ID, FindNext.LABEL, { primary: KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] } + secondary: [KeyMod.Shift | KeyCode.Enter], + mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3, KeyMod.Shift | KeyCode.Enter] } }, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find next'); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, FindPrevious.ID_TERMINAL_FOCUS, FindPrevious.LABEL, { primary: KeyMod.Shift | KeyCode.F3, @@ -534,7 +539,8 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, Fi }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find previous', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, FindPrevious.ID, FindPrevious.LABEL, { primary: KeyMod.Shift | KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, + secondary: [KeyCode.Enter], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3, KeyCode.Enter] }, }, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find previous'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 41ceb3e71b1bc..2492bd5804124 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { Action, IAction } from 'vs/base/common/actions'; import { EndOfLinePreference } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { ITerminalService, TERMINAL_PANEL_ID, ITerminalInstance, Direction, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalService, TERMINAL_PANEL_ID, ITerminalInstance, Direction, ITerminalConfigHelper, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { TogglePanelAction } from 'vs/workbench/browser/panel'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -993,10 +993,10 @@ export class ClearSelectionTerminalAction extends Action { } } -export class AllowWorkspaceShellTerminalCommand extends Action { +export class ManageWorkspaceShellPermissionsTerminalCommand extends Action { - public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_ALLOW; - public static readonly LABEL = nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration"); + public static readonly ID = TERMINAL_COMMAND_ID.MANAGE_WORKSPACE_SHELL_PERMISSIONS; + public static readonly LABEL = nls.localize('workbench.action.terminal.manageWorkspaceShellPermissions', "Manage Workspace Shell Permissions"); constructor( id: string, label: string, @@ -1005,27 +1005,8 @@ export class AllowWorkspaceShellTerminalCommand extends Action { super(id, label); } - public run(event?: any): Promise { - this.terminalService.setWorkspaceShellAllowed(true); - return Promise.resolve(undefined); - } -} - -export class DisallowWorkspaceShellTerminalCommand extends Action { - - 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( - id: string, label: string, - @ITerminalService private readonly terminalService: ITerminalService - ) { - super(id, label); - } - - public run(event?: any): Promise { - this.terminalService.setWorkspaceShellAllowed(false); - return Promise.resolve(undefined); + public async run(event?: any): Promise { + await this.terminalService.manageWorkspaceShellPermissions(); } } @@ -1053,7 +1034,7 @@ export class RenameTerminalAction extends Action { prompt: nls.localize('workbench.action.terminal.rename.prompt', "Enter terminal name"), }).then(name => { if (name) { - terminalInstance.setTitle(name, false); + terminalInstance.setTitle(name, TitleEventSource.Api); } }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 7d7aafdb6b39b..e4a307e4059f1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -11,7 +11,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { ITerminalConfiguration, ITerminalFont, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MINIMUM_LETTER_SPACING, LinuxDistro, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal'; import Severity from 'vs/base/common/severity'; import { Terminal as XTermTerminal } from 'xterm'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import { IBrowserTerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminal'; import { Emitter, Event } from 'vs/base/common/event'; import { basename } from 'vs/base/common/path'; @@ -254,7 +254,6 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { return r; } - private readonly NO_RECOMMENDATIONS_KEY = 'terminalConfigHelper/launchRecommendationsIgnore'; private recommendationsShown = false; public async showRecommendations(shellLaunchConfig: IShellLaunchConfig): Promise { @@ -264,10 +263,6 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { this.recommendationsShown = true; if (platform.isWindows && shellLaunchConfig.executable && basename(shellLaunchConfig.executable).toLowerCase() === 'wsl.exe') { - if (this._storageService.getBoolean(this.NO_RECOMMENDATIONS_KEY, StorageScope.WORKSPACE, false)) { - return; - } - if (! await this.isExtensionInstalled('ms-vscode-remote.remote-wsl')) { this._notificationService.prompt( Severity.Info, @@ -276,16 +271,10 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { "Check out the 'Visual Studio Code Remote - WSL' extension for a great development experience in WSL. Click [here]({0}) to learn more.", 'https://go.microsoft.com/fwlink/?linkid=2097212' ), - [ - { - label: nls.localize('doNotShowAgain', "Don't Show Again"), - run: () => { - this._storageService.store(this.NO_RECOMMENDATIONS_KEY, true, StorageScope.WORKSPACE); - } - } - ], + [], { - sticky: true + sticky: true, + neverShowAgain: { id: 'terminalConfigHelper/launchRecommendationsIgnore', scope: NeverShowAgainScope.WORKSPACE } } ); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 87ca61f2d5517..a3d01c1c6e4fe 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -19,7 +19,7 @@ export class TerminalFindWidget extends SimpleFindWidget { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITerminalService private readonly _terminalService: ITerminalService ) { - super(_contextViewService, _contextKeyService, findState, true, true); + super(_contextViewService, _contextKeyService, findState, true); this._register(findState.onFindReplaceStateChange(() => { this.show(); })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 9ca4fa8fb71a0..c210bb9d05dc8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -25,7 +25,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager'; -import { IShellLaunchConfig, ITerminalDimensions, ITerminalInstance, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_PANEL_ID, IWindowsShellHelper, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalDimensions, ITerminalInstance, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_PANEL_ID, IWindowsShellHelper, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminalCommands'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; @@ -500,7 +500,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Force line data to be sent when the cursor is moved, the main purpose for // this is because ConPTY will often not do a line feed but instead move the // cursor, in which case we still want to send the current line's data to tasks. - xterm.addCsiHandler('H', () => { + xterm.parser.addCsiHandler({ final: 'H' }, () => { this._onCursorMove(); return false; }); @@ -865,7 +865,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } this.focus(); - this._xterm._core._coreService.triggerDataEvent(await this._clipboardService.readText(), true); + this._xterm.paste(await this._clipboardService.readText()); } public write(text: string): void { @@ -973,11 +973,22 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e)); if (this._shellLaunchConfig.name) { - this.setTitle(this._shellLaunchConfig.name, false); + this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Only listen for process title changes when a name is not provided - this.setTitle(this._shellLaunchConfig.executable, true); - this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.setTitle(title ? title : '', true)); + if (this._configHelper.config.experimentalUseTitleEvent) { + this._processManager.ptyProcessReady.then(() => { + this._terminalInstanceService.getDefaultShellAndArgs(false).then(e => { + this.setTitle(e.shell, TitleEventSource.Sequence); + }); + this._xtermReadyPromise.then(xterm => { + this._messageTitleDisposable = xterm.onTitleChange(e => this._onTitleChange(e)); + }); + }); + } else { + this.setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); + this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.setTitle(title ? title : '', TitleEventSource.Process)); + } } if (platform.isWindows) { @@ -1149,7 +1160,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._createProcess(); if (oldTitle !== this._title) { - this.setTitle(this._title, true); + this.setTitle(this._title, TitleEventSource.Process); } this._processManager.onProcessData(data => this._onProcessData(data)); @@ -1168,6 +1179,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._sendLineData(buffer, buffer.baseY + buffer.cursorY); } + private _onTitleChange(title: string): void { + if (this.isTitleSetByProcess) { + this.setTitle(title, TitleEventSource.Sequence); + } + } + private _sendLineData(buffer: IBuffer, lineIndex: number): void { let line = buffer.getLine(lineIndex); if (!line) { @@ -1348,23 +1365,26 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(cols, rows)); } - public setTitle(title: string | undefined, eventFromProcess: boolean): void { + public setTitle(title: string | undefined, eventSource: TitleEventSource): void { if (!title) { return; } - if (eventFromProcess) { - title = path.basename(title); - if (platform.isWindows) { - // Remove the .exe extension - title = title.split('.exe')[0]; - } - } else { - // If the title has not been set by the API or the rename command, unregister the handler that - // automatically updates the terminal name - dispose(this._messageTitleDisposable); - this._messageTitleDisposable = undefined; - dispose(this._windowsShellHelper); - this._windowsShellHelper = undefined; + switch (eventSource) { + case TitleEventSource.Process: + title = path.basename(title); + if (platform.isWindows) { + // Remove the .exe extension + title = title.split('.exe')[0]; + } + break; + case TitleEventSource.Api: + // If the title has not been set by the API or the rename command, unregister the handler that + // automatically updates the terminal name + dispose(this._messageTitleDisposable); + this._messageTitleDisposable = undefined; + dispose(this._windowsShellHelper); + this._windowsShellHelper = undefined; + break; } const didTitleChange = title !== this._title; this._title = title; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts b/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts index 334c3ab0b1352..5c3eae374efb5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts @@ -276,7 +276,7 @@ export class TerminalPanel extends Panel { const resources = e.dataTransfer.getData(DataTransfers.RESOURCES); if (resources) { path = URI.parse(JSON.parse(resources)[0]).fsPath; - } else if (e.dataTransfer.files.length > 0) { + } else if (e.dataTransfer.files.length > 0 && e.dataTransfer.files[0].path /* Electron only */) { // Check if the file was dragged from the filesystem path = URI.file(e.dataTransfer.files[0].path).fsPath; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index b3ff7db5e88e2..d99d8de0ab15f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -5,6 +5,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { env as processEnv } from 'vs/base/common/process'; import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent, ITerminalEnvironment, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; @@ -225,7 +226,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const envFromConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.env.${platformKey}`); const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(); this._configHelper.showRecommendations(shellLaunchConfig); - const baseEnv = this._configHelper.config.inheritEnv ? process.env as platform.IProcessEnvironment : await this._terminalInstanceService.getMainProcessParentEnv(); + const baseEnv = this._configHelper.config.inheritEnv ? processEnv : await this._terminalInstanceService.getMainProcessParentEnv(); const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, lastActiveWorkspace, envFromConfigValue, this._configurationResolverService, isWorkspaceShellAllowed, this._productService.version, this._configHelper.config.setLocaleVariables, baseEnv); const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index bfa25469cea38..daed808061268 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -26,6 +26,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur export class TerminalService extends CommonTerminalService implements ITerminalService { private _configHelper: IBrowserTerminalConfigHelper; + private _terminalContainer: HTMLElement | undefined; public get configHelper(): ITerminalConfigHelper { return this._configHelper; } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index bb99965b26c2f..9d582c1ad6782 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -117,6 +117,7 @@ export interface ITerminalConfiguration { splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; experimentalRefreshOnResume: boolean; + experimentalUseTitleEvent: boolean; } export interface ITerminalConfigHelper { @@ -247,6 +248,7 @@ export interface ITerminalService { /** * Creates a raw terminal instance, this should not be used outside of the terminal part. */ + // tslint:disable-next-line: no-dom-globals createInstance(container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance; getInstanceFromId(terminalId: number): ITerminalInstance | undefined; getInstanceFromIndex(terminalIndex: number): ITerminalInstance; @@ -278,8 +280,9 @@ export interface ITerminalService { selectDefaultWindowsShell(): Promise; + // tslint:disable-next-line: no-dom-globals setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; - setWorkspaceShellAllowed(isAllowed: boolean): void; + manageWorkspaceShellPermissions(): void; /** * Takes a path and returns the properly escaped path to send to the terminal. @@ -336,6 +339,7 @@ export interface ITerminalTab { focusNextPane(): void; resizePane(direction: Direction): void; setActiveInstanceByIndex(index: number): void; + // tslint:disable-next-line: no-dom-globals attachToElement(element: HTMLElement): void; setVisible(visible: boolean): void; layout(width: number, height: number): void; @@ -610,6 +614,7 @@ export interface ITerminalInstance { * * @param container The element to attach the terminal instance to. */ + // tslint:disable-next-line: no-dom-globals attachToElement(container: HTMLElement): void; /** @@ -636,7 +641,7 @@ export interface ITerminalInstance { /** * Sets the title of the terminal instance. */ - setTitle(title: string, eventFromProcess: boolean): void; + setTitle(title: string, eventSource: TitleEventSource): void; waitForTitle(): Promise; @@ -769,6 +774,15 @@ export enum LinuxDistro { Unknown } +export enum TitleEventSource { + /** From the API or the rename command that overrides any other type */ + Api, + /** From the process name property*/ + Process, + /** From the VT sequence */ + Sequence +} + export interface IWindowsShellHelper extends IDisposable { getShellName(): Promise; } diff --git a/src/vs/workbench/contrib/terminal/common/terminalCommands.ts b/src/vs/workbench/contrib/terminal/common/terminalCommands.ts index 60da258805df0..ddd108304ae11 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalCommands.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalCommands.ts @@ -48,8 +48,7 @@ export const enum TERMINAL_COMMAND_ID { 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', + MANAGE_WORKSPACE_SHELL_PERMISSIONS = 'workbench.action.terminal.manageWorkspaceShellPermissions', RENAME = 'workbench.action.terminal.rename', FIND_WIDGET_FOCUS = 'workbench.action.terminal.focusFindWidget', FIND_WIDGET_HIDE = 'workbench.action.terminal.hideFindWidget', diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 9d6215af30c98..6b9a3ec87b742 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -62,7 +62,7 @@ export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, ve env['COLORTERM'] = 'truecolor'; } -function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnvironment | NodeJS.ProcessEnv | undefined) { +function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnvironment | undefined) { if (!other) { return; } @@ -144,18 +144,20 @@ export function getCwd( try { customCwd = configurationResolverService.resolve(lastActiveWorkspace, customCwd); } catch (e) { - // There was an issue resolving a variable, just use the unresolved customCwd which - // which will fail, and log the error in the console. + // There was an issue resolving a variable, log the error in the console and + // fallback to the default. if (logService) { logService.error('Could not resolve terminal.integrated.cwd', e); } - return customCwd; + customCwd = undefined; } } - if (path.isAbsolute(customCwd)) { - cwd = customCwd; - } else if (root) { - cwd = path.join(root.fsPath, customCwd); + if (customCwd) { + if (path.isAbsolute(customCwd)) { + cwd = customCwd; + } else if (root) { + cwd = path.join(root.fsPath, customCwd); + } } } @@ -208,33 +210,33 @@ export function getDefaultShell( if (!maybeExecutable) { maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'shell', platformOverride); } - maybeExecutable = maybeExecutable || defaultShell; + let executable: string = maybeExecutable || defaultShell; // Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's // safe to assume that this was used by accident as Sysnative does not // exist and will break the terminal in non-WoW64 environments. if ((platformOverride === platform.Platform.Windows) && !isWoW64 && windir) { const sysnativePath = path.join(windir, 'Sysnative').replace(/\//g, '\\').toLowerCase(); - if (maybeExecutable && maybeExecutable.toLowerCase().indexOf(sysnativePath) === 0) { - maybeExecutable = path.join(windir, 'System32', maybeExecutable.substr(sysnativePath.length + 1)); + if (executable && executable.toLowerCase().indexOf(sysnativePath) === 0) { + executable = path.join(windir, 'System32', executable.substr(sysnativePath.length + 1)); } } // Convert / to \ on Windows for convenience - if (maybeExecutable && platformOverride === platform.Platform.Windows) { - maybeExecutable = maybeExecutable.replace(/\//g, '\\'); + if (executable && platformOverride === platform.Platform.Windows) { + executable = executable.replace(/\//g, '\\'); } if (configurationResolverService) { try { - maybeExecutable = configurationResolverService.resolve(lastActiveWorkspace, maybeExecutable); + executable = configurationResolverService.resolve(lastActiveWorkspace, executable); } catch (e) { logService.error(`Could not resolve shell`, e); - maybeExecutable = maybeExecutable; + executable = executable; } } - return maybeExecutable; + return executable; } export function getDefaultShellArgs( diff --git a/src/vs/workbench/contrib/terminal/common/terminalService.ts b/src/vs/workbench/contrib/terminal/common/terminalService.ts index 1b04048fdcc09..74d2b5728dc62 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalService.ts @@ -35,7 +35,6 @@ export abstract class TerminalService implements ITerminalService { protected _isShuttingDown: boolean; protected _terminalFocusContextKey: IContextKey; protected _findWidgetVisible: IContextKey; - protected _terminalContainer: HTMLElement | undefined; protected _terminalTabs: ITerminalTab[] = []; protected _backgroundedTerminalInstances: ITerminalInstance[] = []; protected get _terminalInstances(): ITerminalInstance[] { @@ -123,7 +122,9 @@ export abstract class TerminalService implements ITerminalService { protected abstract _showBackgroundTerminal(instance: ITerminalInstance): void; public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + // tslint:disable-next-line: no-dom-globals public abstract createInstance(container: HTMLElement, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance; + // tslint:disable-next-line: no-dom-globals public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; public getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance { @@ -479,22 +480,28 @@ export abstract class TerminalService implements ITerminalService { return terminalIndex; } - public setWorkspaceShellAllowed(isAllowed: boolean): void { - this.configHelper.setWorkspaceShellAllowed(isAllowed); + public async manageWorkspaceShellPermissions(): Promise { + const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration") }; + const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.terminal.disallowWorkspaceShell', "Disallow Workspace Shell Configuration") }; + const value = await this._quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); + if (!value) { + return; + } + this.configHelper.setWorkspaceShellAllowed(value === allowItem); } - protected _showTerminalCloseConfirmation(): Promise { - let message; + protected async _showTerminalCloseConfirmation(): Promise { + let message: string; if (this.terminalInstances.length === 1) { message = nls.localize('terminalService.terminalCloseConfirmationSingular', "There is an active terminal session, do you want to kill it?"); } else { message = nls.localize('terminalService.terminalCloseConfirmationPlural', "There are {0} active terminal sessions, do you want to kill them?", this.terminalInstances.length); } - - return this._dialogService.confirm({ + const res = await this._dialogService.confirm({ message, type: 'warning', - }).then(res => !res.confirmed); + }); + return !res.confirmed; } protected _showNotEnoughSpaceToast(): void { diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index 2fbc8a25e7462..86a63a0b8439b 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -89,7 +89,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess }, async (err) => { if (err && err.code === 'ENOENT') { let cwd = shellLaunchConfig.cwd instanceof URI ? shellLaunchConfig.cwd.path : shellLaunchConfig.cwd!; - const executable = await findExecutable(shellLaunchConfig.executable!, cwd); + // Try to get path + const envPaths: string[] | undefined = (shellLaunchConfig.env && shellLaunchConfig.env.PATH) ? shellLaunchConfig.env.PATH.split(path.delimiter) : undefined; + const executable = await findExecutable(shellLaunchConfig.executable!, cwd, envPaths); if (!executable) { return Promise.reject(SHELL_PATH_INVALID_EXIT_CODE); } diff --git a/src/vs/workbench/contrib/terminal/node/terminalRemote.ts b/src/vs/workbench/contrib/terminal/node/terminalRemote.ts index e264cdfdd5d5d..3ebea6304cf6e 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalRemote.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalRemote.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { TERMINAL_ACTION_CATEGORY, ITerminalService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_ACTION_CATEGORY, ITerminalService, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminalCommands'; import { Action } from 'vs/base/common/actions'; import { URI } from 'vs/base/common/uri'; @@ -39,7 +39,7 @@ export class CreateNewLocalTerminalAction extends Action { const disposable = instance.onTitleChanged(() => { if (instance.title && instance.title.trim().length > 0) { disposable.dispose(); - instance.setTitle(`${instance.title} (Local)`, false); + instance.setTitle(`${instance.title} (Local)`, TitleEventSource.Api); } }); diff --git a/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts b/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts index 18c24058910eb..0b20c735a89c3 100644 --- a/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts +++ b/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts @@ -5,9 +5,9 @@ import * as platform from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; -import { ITerminalInstance, IWindowsShellHelper } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalInstance, IWindowsShellHelper, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import { Terminal as XTermTerminal } from 'xterm'; -import WindowsProcessTreeType = require('windows-process-tree'); +import * as WindowsProcessTreeType from 'windows-process-tree'; import { Disposable } from 'vs/base/common/lifecycle'; const SHELL_EXECUTABLES = [ @@ -80,7 +80,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe if (platform.isWindows && this._terminalInstance.isTitleSetByProcess) { this.getShellName().then(title => { if (!this._isDisposed) { - this._terminalInstance.setTitle(title, true); + this._terminalInstance.setTitle(title, TitleEventSource.Process); } }); } diff --git a/src/vs/workbench/contrib/terminal/test/electron-browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/electron-browser/terminalConfigHelper.test.ts index f1984496f23eb..0babdeccff0ac 100644 --- a/src/vs/workbench/contrib/terminal/test/electron-browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/electron-browser/terminalConfigHelper.test.ts @@ -16,14 +16,14 @@ suite('Workbench - TerminalConfigHelper', () => { fixture = document.body; }); - test('TerminalConfigHelper - getFont fontFamily', function () { - const configurationService = new TestConfigurationService(); - configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); - configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: 'bar' } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!); - configHelper.panelContainer = fixture; - assert.equal(configHelper.getFont().fontFamily, 'bar', 'terminal.integrated.fontFamily should be selected over editor.fontFamily'); - }); + // test('TerminalConfigHelper - getFont fontFamily', function () { + // const configurationService = new TestConfigurationService(); + // configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); + // configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: 'bar' } }); + // const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!); + // configHelper.panelContainer = fixture; + // assert.equal(configHelper.getFont().fontFamily, 'bar', 'terminal.integrated.fontFamily should be selected over editor.fontFamily'); + // }); test('TerminalConfigHelper - getFont fontFamily (Linux Fedora)', function () { const configurationService = new TestConfigurationService(); @@ -233,4 +233,4 @@ suite('Workbench - TerminalConfigHelper', () => { configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts index 65d12c8087724..b91e684cfdae2 100644 --- a/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts @@ -28,24 +28,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { generateUuid } from 'vs/base/common/uuid'; -function renderBody( - body: string, - css: string -): string { - const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://', 'vscode-resource://'); - return ` - - - - - - - - - ${body} - `; -} - export class ReleaseNotesManager { private readonly _releaseNotesCache = new Map>(); @@ -188,8 +170,18 @@ export class ReleaseNotesManager { const content = await this.renderContent(text); const colorMap = TokenizationRegistry.getColorMap(); const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; - const body = renderBody(content, css); - return body; + const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://', 'vscode-resource://'); + return ` + + + + + + + + + ${content} + `; } private async renderContent(text: string): Promise { diff --git a/src/vs/workbench/contrib/update/electron-browser/update.ts b/src/vs/workbench/contrib/update/electron-browser/update.ts index 0d2d53003b725..d429d21347def 100644 --- a/src/vs/workbench/contrib/update/electron-browser/update.ts +++ b/src/vs/workbench/contrib/update/electron-browser/update.ts @@ -19,7 +19,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; import * as semver from 'semver-umd'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { INotificationService, INotificationHandle, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ReleaseNotesManager } from './releaseNotesEditor'; @@ -119,7 +119,7 @@ export class ProductContribution implements IWorkbenchContribution { @IStorageService storageService: IStorageService, @IInstantiationService instantiationService: IInstantiationService, @INotificationService notificationService: INotificationService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IOpenerService openerService: IOpenerService, @IConfigurationService configurationService: IConfigurationService, @IWindowService windowService: IWindowService, @@ -162,32 +162,8 @@ export class ProductContribution implements IWorkbenchContribution { } } -class NeverShowAgain { - - private readonly key: string; - - readonly action = new Action(`neverShowAgain:${this.key}`, nls.localize('neveragain', "Don't Show Again"), undefined, true, (notification: INotificationHandle) => { - - // Hide notification - notification.close(); - - this.storageService.store(this.key, true, StorageScope.GLOBAL); - - return Promise.resolve(true); - }); - - constructor(key: string, @IStorageService private readonly storageService: IStorageService) { - this.key = `neverShowAgain:${key}`; - } - - shouldShow(): boolean { - return !this.storageService.getBoolean(this.key, StorageScope.GLOBAL, false); - } -} - export class Win3264BitContribution implements IWorkbenchContribution { - private static readonly KEY = 'update/win32-64bits'; private static readonly URL = 'https://code.visualstudio.com/updates/v1_15#_windows-64-bit'; private static readonly INSIDER_URL = 'https://github.com/Microsoft/vscode-docs/blob/vnext/release-notes/v1_15.md#windows-64-bit'; @@ -200,28 +176,18 @@ export class Win3264BitContribution implements IWorkbenchContribution { return; } - const neverShowAgain = new NeverShowAgain(Win3264BitContribution.KEY, storageService); - - if (!neverShowAgain.shouldShow()) { - return; - } - const url = product.quality === 'insider' ? Win3264BitContribution.INSIDER_URL : Win3264BitContribution.URL; - const handle = notificationService.prompt( + notificationService.prompt( severity.Info, nls.localize('64bitisavailable', "{0} for 64-bit Windows is now available! Click [here]({1}) to learn more.", product.nameShort, url), - [{ - label: nls.localize('neveragain', "Don't Show Again"), - isSecondary: true, - run: () => { - neverShowAgain.action.run(handle); - neverShowAgain.action.dispose(); - } - }], - { sticky: true } + [], + { + sticky: true, + neverShowAgain: { id: 'neverShowAgain:update/win32-64bits', isSecondary: true } + } ); } } @@ -396,23 +362,13 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu } // windows fast updates (target === system) - const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); - - if (!neverShowAgain.shouldShow()) { - return; - } - - const handle = this.notificationService.prompt( + this.notificationService.prompt( severity.Info, nls.localize('updateInstalling', "{0} {1} is being installed in the background; we'll let you know when it's done.", product.nameLong, update.productVersion), - [{ - label: nls.localize('neveragain', "Don't Show Again"), - isSecondary: true, - run: () => { - neverShowAgain.action.run(handle); - neverShowAgain.action.dispose(); - } - }] + [], + { + neverShowAgain: { id: 'neverShowAgain:update/win32-fast-updates', isSecondary: true } + } ); } diff --git a/src/vs/workbench/contrib/url/common/url.contribution.ts b/src/vs/workbench/contrib/url/common/url.contribution.ts index cbed9bef6a339..af92998139da6 100644 --- a/src/vs/workbench/contrib/url/common/url.contribution.ts +++ b/src/vs/workbench/contrib/url/common/url.contribution.ts @@ -4,24 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -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 { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { IURLService } from 'vs/platform/url/common/url'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { URI } from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IProductService } from 'vs/platform/product/common/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; +import Severity from 'vs/base/common/severity'; export class OpenUrlAction extends Action { - static readonly ID = 'workbench.action.url.openUrl'; - static readonly LABEL = localize('openUrl', "Open URL"); + static readonly LABEL = localize('openUrl', 'Open URL'); constructor( id: string, label: string, @IURLService private readonly urlService: IURLService, - @IQuickInputService private readonly quickInputService: IQuickInputService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(id, label); } @@ -34,5 +44,194 @@ export class OpenUrlAction extends Action { } } -Registry.as(ActionExtensions.WorkbenchActions) - .registerWorkbenchAction(new SyncActionDescriptor(OpenUrlAction, OpenUrlAction.ID, OpenUrlAction.LABEL), 'Open URL', localize('developer', "Developer")); \ No newline at end of file +Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction( + new SyncActionDescriptor(OpenUrlAction, OpenUrlAction.ID, OpenUrlAction.LABEL), + 'Open URL', + localize('developer', 'Developer') +); + +const DEAFULT_TRUSTED_DOMAINS = [ + 'https://code.visualstudio.com', + 'https://go.microsoft.com', + 'https://github.com', + 'https://marketplace.visualstudio.com', + 'https://vscode-auth.github.com' +]; + +const configureTrustedDomainsHandler = async ( + quickInputService: IQuickInputService, + storageService: IStorageService, + domainToConfigure?: string +) => { + let trustedDomains: string[] = DEAFULT_TRUSTED_DOMAINS; + + try { + const trustedDomainsSrc = storageService.get('http.trustedDomains', StorageScope.GLOBAL); + if (trustedDomainsSrc) { + trustedDomains = JSON.parse(trustedDomainsSrc); + } + } catch (err) { } + + const domainQuickPickItems: IQuickPickItem[] = trustedDomains + .filter(d => d !== '*') + .map(d => { + return { + type: 'item', + label: d, + id: d, + picked: true + }; + }); + + const specialQuickPickItems: IQuickPickItem[] = [ + { + type: 'item', + label: localize('openAllLinksWithoutPrompt', 'Open all links without prompt'), + id: '*', + picked: trustedDomains.indexOf('*') !== -1 + } + ]; + + let domainToConfigureItem: IQuickPickItem | undefined = undefined; + if (domainToConfigure && trustedDomains.indexOf(domainToConfigure) === -1) { + domainToConfigureItem = { + type: 'item', + label: domainToConfigure, + id: domainToConfigure, + picked: true, + description: localize('trustDomainAndOpenLink', 'Trust domain and open link') + }; + specialQuickPickItems.push(domainToConfigureItem); + } + + const quickPickItems: (IQuickPickItem | IQuickPickSeparator)[] = + domainQuickPickItems.length === 0 + ? specialQuickPickItems + : [...specialQuickPickItems, { type: 'separator' }, ...domainQuickPickItems]; + + const pickedResult = await quickInputService.pick(quickPickItems, { + canPickMany: true, + activeItem: domainToConfigureItem + }); + + if (pickedResult) { + const pickedDomains: string[] = pickedResult.map(r => r.id!); + storageService.store('http.trustedDomains', JSON.stringify(pickedDomains), StorageScope.GLOBAL); + + return pickedDomains; + } + + return []; +}; + +const configureTrustedDomainCommand = { + id: 'workbench.action.configureTrustedDomains', + description: { + description: localize('configureTrustedDomains', 'Configure Trusted Domains'), + args: [{ name: 'domainToConfigure', schema: { type: 'string' } }] + }, + handler: (accessor: ServicesAccessor, domainToConfigure?: string) => { + const quickInputService = accessor.get(IQuickInputService); + const storageService = accessor.get(IStorageService); + + return configureTrustedDomainsHandler(quickInputService, storageService, domainToConfigure); + } +}; + +CommandsRegistry.registerCommand(configureTrustedDomainCommand); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: configureTrustedDomainCommand.id, + title: configureTrustedDomainCommand.description.description + } +}); + +class OpenerValidatorContributions implements IWorkbenchContribution { + constructor( + @IOpenerService private readonly _openerService: IOpenerService, + @IStorageService private readonly _storageService: IStorageService, + @IDialogService private readonly _dialogService: IDialogService, + @IProductService private readonly _productService: IProductService, + @IQuickInputService private readonly _quickInputService: IQuickInputService + ) { + this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); + } + + async validateLink(resource: URI): Promise { + const { scheme, authority } = resource; + + if (!equalsIgnoreCase(scheme, Schemas.http) && !equalsIgnoreCase(scheme, Schemas.https)) { + return true; + } + + let trustedDomains: string[] = DEAFULT_TRUSTED_DOMAINS; + try { + const trustedDomainsSrc = this._storageService.get('http.trustedDomains', StorageScope.GLOBAL); + if (trustedDomainsSrc) { + trustedDomains = JSON.parse(trustedDomainsSrc); + } + } catch (err) { } + + const domainToOpen = `${scheme}://${authority}`; + + if (isDomainTrusted(domainToOpen, trustedDomains)) { + return true; + } else { + const choice = await this._dialogService.show( + Severity.Info, + localize( + 'openExternalLinkAt', + 'Do you want {0} to open the external website?\n{1}', + this._productService.nameShort, + resource.toString(true) + ), + [ + localize('openLink', 'Open Link'), + localize('cancel', 'Cancel'), + localize('configureTrustedDomains', 'Configure Trusted Domains') + ], + { + cancelId: 1 + } + ); + + // Open Link + if (choice === 0) { + return true; + } + // Configure Trusted Domains + else if (choice === 2) { + const pickedDomains = await configureTrustedDomainsHandler(this._quickInputService, this._storageService, domainToOpen); + if (pickedDomains.indexOf(domainToOpen) !== -1) { + return true; + } + return false; + } + + return false; + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + OpenerValidatorContributions, + LifecyclePhase.Restored +); + +/** + * Check whether a domain like https://www.microsoft.com matches + * the list of trusted domains. + */ +function isDomainTrusted(domain: string, trustedDomains: string[]) { + for (let i = 0; i < trustedDomains.length; i++) { + if (trustedDomains[i] === '*') { + return true; + } + + if (trustedDomains[i] === domain) { + return true; + } + } + + return false; +} diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index 54c9b8620f5db..5a1ee63bf03d3 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./watermark'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; import { isMacintosh, OS } from 'vs/base/common/platform'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -37,51 +37,17 @@ interface WatermarkEntry { mac?: boolean; } -const showCommands: WatermarkEntry = { - text: nls.localize('watermark.showCommands', "Show All Commands"), - id: ShowAllCommandsAction.ID -}; -const quickOpen: WatermarkEntry = { - text: nls.localize('watermark.quickOpen', "Go to File"), - id: QUICKOPEN_ACTION_ID -}; -const openFileNonMacOnly: WatermarkEntry = { - text: nls.localize('watermark.openFile', "Open File"), - id: OpenFileAction.ID, - mac: false -}; -const openFolderNonMacOnly: WatermarkEntry = { - text: nls.localize('watermark.openFolder', "Open Folder"), - id: OpenFolderAction.ID, - mac: false -}; -const openFileOrFolderMacOnly: WatermarkEntry = { - text: nls.localize('watermark.openFileFolder', "Open File or Folder"), - id: OpenFileFolderAction.ID, - mac: true -}; -const openRecent: WatermarkEntry = { - text: nls.localize('watermark.openRecent', "Open Recent"), - id: 'workbench.action.openRecent' -}; -const newUntitledFile: WatermarkEntry = { - text: nls.localize('watermark.newUntitledFile', "New Untitled File"), - id: GlobalNewUntitledFileAction.ID -}; +const showCommands: WatermarkEntry = { text: nls.localize('watermark.showCommands', "Show All Commands"), id: ShowAllCommandsAction.ID }; +const quickOpen: WatermarkEntry = { text: nls.localize('watermark.quickOpen', "Go to File"), id: QUICKOPEN_ACTION_ID }; +const openFileNonMacOnly: WatermarkEntry = { text: nls.localize('watermark.openFile', "Open File"), id: OpenFileAction.ID, mac: false }; +const openFolderNonMacOnly: WatermarkEntry = { text: nls.localize('watermark.openFolder', "Open Folder"), id: OpenFolderAction.ID, mac: false }; +const openFileOrFolderMacOnly: WatermarkEntry = { text: nls.localize('watermark.openFileFolder', "Open File or Folder"), id: OpenFileFolderAction.ID, mac: true }; +const openRecent: WatermarkEntry = { text: nls.localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' }; +const newUntitledFile: WatermarkEntry = { text: nls.localize('watermark.newUntitledFile', "New Untitled File"), id: GlobalNewUntitledFileAction.ID }; const newUntitledFileMacOnly: WatermarkEntry = assign({ mac: true }, newUntitledFile); -const toggleTerminal: WatermarkEntry = { - text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), - id: TERMINAL_COMMAND_ID.TOGGLE -}; - -const findInFiles: WatermarkEntry = { - text: nls.localize('watermark.findInFiles', "Find in Files"), - id: FindInFilesActionId -}; -const startDebugging: WatermarkEntry = { - text: nls.localize('watermark.startDebugging', "Start Debugging"), - id: StartAction.ID -}; +const toggleTerminal: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: TERMINAL_COMMAND_ID.TOGGLE }; +const findInFiles: WatermarkEntry = { text: nls.localize('watermark.findInFiles', "Find in Files"), id: FindInFilesActionId }; +const startDebugging: WatermarkEntry = { text: nls.localize('watermark.startDebugging', "Start Debugging"), id: StartAction.ID }; const noFolderEntries = [ showCommands, @@ -103,13 +69,13 @@ const folderEntries = [ const WORKBENCH_TIPS_ENABLED_KEY = 'workbench.tips.enabled'; export class WatermarkContribution extends Disposable implements IWorkbenchContribution { - private watermark: HTMLElement; + private watermarkDisposable = this._register(new DisposableStore()); private enabled: boolean; private workbenchState: WorkbenchState; constructor( - @ILifecycleService lifecycleService: ILifecycleService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -117,13 +83,20 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService ) { super(); - this.workbenchState = contextService.getWorkbenchState(); - lifecycleService.onShutdown(this.dispose, this); + this.workbenchState = contextService.getWorkbenchState(); this.enabled = this.configurationService.getValue(WORKBENCH_TIPS_ENABLED_KEY); + + this.registerListeners(); + if (this.enabled) { this.create(); } + } + + private registerListeners(): void { + this.lifecycleService.onShutdown(this.dispose, this); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(WORKBENCH_TIPS_ENABLED_KEY)) { const enabled = this.configurationService.getValue(WORKBENCH_TIPS_ENABLED_KEY); @@ -137,6 +110,7 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr } } })); + this._register(this.contextService.onDidChangeWorkbenchState(e => { const previousWorkbenchState = this.workbenchState; this.workbenchState = this.contextService.getWorkbenchState(); @@ -157,6 +131,7 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr const selected = folder ? folderEntries : noFolderEntries .filter(entry => !('mac' in entry) || entry.mac === isMacintosh) .filter(entry => !!CommandsRegistry.getCommand(entry.id)); + const update = () => { dom.clearNode(box); selected.map(entry => { @@ -169,10 +144,14 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr dd.innerHTML = keybinding.element.outerHTML; }); }; + update(); + dom.prepend(container.firstElementChild as HTMLElement, this.watermark); - this._register(this.keybindingService.onDidUpdateKeybindings(update)); - this._register(this.editorGroupsService.onDidLayout(dimension => this.handleEditorPartSize(container, dimension))); + + this.watermarkDisposable.add(this.keybindingService.onDidUpdateKeybindings(update)); + this.watermarkDisposable.add(this.editorGroupsService.onDidLayout(dimension => this.handleEditorPartSize(container, dimension))); + this.handleEditorPartSize(container, this.editorGroupsService.contentDimension); } @@ -187,9 +166,11 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr private destroy(): void { if (this.watermark) { this.watermark.remove(); + const container = this.layoutService.getContainer(Parts.EDITOR_PART); container.classList.remove('has-watermark'); - this.dispose(); + + this.watermarkDisposable.clear(); } } diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index c86d25f7724f3..ead4c72d910d5 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IWebviewService, Webview, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, Webview, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { memoize } from 'vs/base/common/decorators'; @@ -153,6 +153,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd reload(): void { this.withWebview(webview => webview.reload()); } showFind(): void { this.withWebview(webview => webview.showFind()); } hideFind(): void { this.withWebview(webview => webview.hideFind()); } + runFindAction(previous: boolean): void { this.withWebview(webview => webview.runFindAction(previous)); } public getInnerWebview() { return this._webview.value; diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index ac53ce590e20e..e301f5ea90d74 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -3,9 +3,6 @@ - - Virtual Document @@ -16,4 +13,4 @@ - \ No newline at end of file + diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 63585fc25c210..c766b83c11493 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -188,6 +188,11 @@ return; } + // Prevent middle clicks opening a broken link in the browser + if (event.button == 1) { + event.preventDefault(); + } + let baseElement = event.view.document.getElementsByTagName('base')[0]; /** @type {any} */ let node = event.target; @@ -280,6 +285,17 @@ applyStyles(newDocument, newDocument.body); + // Check for CSP + const csp = newDocument.querySelector('meta[http-equiv="Content-Security-Policy"]'); + if (!csp) { + host.postMessage('no-csp-found'); + } else { + // Rewrite vscode-resource in csp + if (data.endpoint) { + csp.setAttribute('content', csp.getAttribute('content').replace(/vscode-resource:/g, data.endpoint)); + } + } + // set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off // and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden return '\n' + newDocument.documentElement.outerHTML; @@ -443,7 +459,7 @@ }); // Bubble out link clicks - newFrame.contentWindow.addEventListener('click', handleInnerClick); + newFrame.contentWindow.addEventListener('mousedown', handleInnerClick); if (host.onIframeLoaded) { host.onIframeLoaded(newFrame); diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts index 40dac14b615dd..e6bf13abce6ae 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -15,8 +15,8 @@ import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } fro import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; -import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/common/webview'; -import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand } from '../browser/webviewCommands'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, webviewDeveloperCategory, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; import { WebviewEditor } from '../browser/webviewEditor'; import { WebviewEditorInput } from '../browser/webviewEditorInput'; import { IWebviewEditorService, WebviewEditorService } from '../browser/webviewEditorService'; @@ -58,6 +58,28 @@ function registerWebViewCommands(editorId: string): void { weight: KeybindingWeight.EditorContrib } })).register(); + + (new WebViewEditorFindNextCommand({ + id: WebViewEditorFindNextCommand.ID, + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new WebViewEditorFindPreviousCommand({ + id: WebViewEditorFindPreviousCommand.ID, + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + })).register(); } registerWebViewCommands(WebviewEditor.ID); diff --git a/src/vs/workbench/contrib/webview/common/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts similarity index 94% rename from src/vs/workbench/contrib/webview/common/webview.ts rename to src/vs/workbench/contrib/webview/browser/webview.ts index d6a73ff5d6131..e7ef6086eaf83 100644 --- a/src/vs/workbench/contrib/webview/common/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -16,6 +16,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' * Set when the find widget in a webview is visible. */ export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); +export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED = new RawContextKey('webviewFindWidgetFocused', false); export const IWebviewService = createDecorator('webviewService'); @@ -41,7 +42,6 @@ export interface IWebviewService { export const WebviewResourceScheme = 'vscode-resource'; export interface WebviewOptions { - readonly allowSvgs?: boolean; readonly extension?: { readonly location: URI; readonly id?: ExtensionIdentifier; @@ -53,7 +53,6 @@ export interface WebviewOptions { export interface WebviewContentOptions { readonly allowScripts?: boolean; - readonly svgWhiteList?: string[]; readonly localResourceRoots?: ReadonlyArray; readonly portMapping?: ReadonlyArray; readonly enableCommandUris?: boolean; @@ -85,6 +84,7 @@ export interface Webview extends IDisposable { showFind(): void; hideFind(): void; + runFindAction(previous: boolean): void; } export interface WebviewElement extends Webview { diff --git a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts index a5888e3e6680f..62a3eecece587 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -32,6 +32,27 @@ export class HideWebViewEditorFindCommand extends Command { } } +export class WebViewEditorFindNextCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.findNext'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.find(false); + } + } +} + +export class WebViewEditorFindPreviousCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.findPrevious'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.find(true); + } + } +} export class ReloadWebviewAction extends Action { static readonly ID = 'workbench.action.webview.reloadWebviewAction'; static readonly LABEL = nls.localize('refreshWebviewLabel', "Reload Webviews"); @@ -62,4 +83,4 @@ function getActiveWebviewEditor(accessor: ServicesAccessor): WebviewEditor | nul const editorService = accessor.get(IEditorService); const activeControl = editorService.activeControl as WebviewEditor; return activeControl.isWebviewEditor ? activeControl : null; -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index d01dc751d00b4..33d33303076bf 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -15,7 +15,7 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions } from 'vs/workbench/common/editor'; import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; -import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/common/webview'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -25,7 +25,6 @@ export class WebviewEditor extends BaseEditor { private readonly _scopedContextKeyService = this._register(new MutableDisposable()); private _findWidgetVisible: IContextKey; - private _editorFrame?: HTMLElement; private _content?: HTMLElement; @@ -79,6 +78,12 @@ export class WebviewEditor extends BaseEditor { this.withWebview(webview => webview.hideFind()); } + public find(previous: boolean) { + this.withWebview(webview => { + webview.runFindAction(previous); + }); + } + public reload() { this.withWebview(webview => webview.reload()); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index c91e93d915ffd..316c112a8bd1c 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; -import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/common/webview'; +import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { UnownedDisposable as Unowned } from 'vs/base/common/lifecycle'; class WebviewIconsManager { @@ -39,11 +39,11 @@ class WebviewIconsManager { this._icons.forEach((value, key) => { const webviewSelector = `.show-file-icons .webview-${key}-name-file-icon::before`; if (URI.isUri(value)) { - cssRules.push(`${webviewSelector} { content: ""; background-image: url(${dom.asDomUri(value).toString()}); }`); + cssRules.push(`${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value)}; }`); } else { - cssRules.push(`.vs ${webviewSelector} { content: ""; background-image: url(${dom.asDomUri(value.light).toString()}); }`); - cssRules.push(`.vs-dark ${webviewSelector} { content: ""; background-image: url(${dom.asDomUri(value.dark).toString()}); }`); + cssRules.push(`.vs ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.light)}; }`); + cssRules.push(`.vs-dark ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.dark)}; }`); } }); this._styleElement.innerHTML = cssRules.join('\n'); diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts index b5f3ce40819ad..a8dbabc2c8726 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts @@ -45,7 +45,7 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { const data: SerializedWebview = { viewType: input.viewType, title: input.getName(), - options: input.webview.options, + options: { ...input.webview.options, ...input.webview.contentOptions }, extensionLocation: input.extension ? input.extension.location : undefined, extensionId: input.extension && input.extension.id ? input.extension.id.value : undefined, state: input.webview.state, @@ -118,4 +118,4 @@ function reviveState(state: unknown | undefined): undefined | string { return (state as any).state; } return undefined; -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts index fb0354fab1af9..6fdda6cfb1cbf 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { GroupIdentifier } from 'vs/workbench/common/editor'; -import { IWebviewService, WebviewOptions, WebviewContentOptions } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, WebviewOptions, WebviewContentOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { RevivedWebviewEditorInput, WebviewEditorInput } from './webviewEditorInput'; @@ -244,7 +244,6 @@ export class WebviewEditorService implements IWebviewEditorService { private createWebiew(id: string, extension: { location: URI; id: ExtensionIdentifier; } | undefined, options: WebviewInputOptions) { return this._webviewService.createWebviewEditorOverlay(id, { - allowSvgs: true, extension: extension, enableFindWidget: options.enableFindWidget, retainContextWhenHidden: options.retainContextWhenHidden diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index aa4473313d4f9..9364cf5c6b47f 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -5,9 +5,9 @@ import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; +import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; @@ -40,14 +40,14 @@ export class IFrameWebview extends Disposable implements Webview { contentOptions: WebviewContentOptions, @IThemeService themeService: IThemeService, @ITunnelService tunnelService: ITunnelService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); const useExternalEndpoint = this._configurationService.getValue('webview.experimental.useExternalEndpoint'); - if (typeof environmentService.webviewEndpoint !== 'string' && !useExternalEndpoint) { + if (!useExternalEndpoint && (!environmentService.options || typeof environmentService.options.webviewEndpoint !== 'string')) { throw new Error('To use iframe based webviews, you must configure `environmentService.webviewEndpoint`'); } @@ -145,7 +145,7 @@ export class IFrameWebview extends Disposable implements Webview { private get endpoint(): string { const useExternalEndpoint = this._configurationService.getValue('webview.experimental.useExternalEndpoint'); - const baseEndpoint = useExternalEndpoint ? 'https://{{uuid}}.vscode-webview-test.com/8fa811108f0f0524c473020ef57b6620f6c201e1' : this.environmentService.webviewEndpoint!; + const baseEndpoint = useExternalEndpoint ? 'https://{{uuid}}.vscode-webview-test.com/8fa811108f0f0524c473020ef57b6620f6c201e1' : this.environmentService.options!.webviewEndpoint!; const endpoint = baseEndpoint.replace('{{uuid}}', this.id); if (endpoint[endpoint.length - 1] === '/') { return endpoint.slice(0, endpoint.length - 1); @@ -202,7 +202,8 @@ export class IFrameWebview extends Disposable implements Webview { this._send('content', { contents: this.content.html, options: this.content.options, - state: this.content.state + state: this.content.state, + endpoint: this.endpoint, }); } @@ -213,7 +214,7 @@ export class IFrameWebview extends Disposable implements Webview { } } - initialScrollProgress: number; + initialScrollProgress: number = 0; private readonly _onDidFocus = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocus.event; @@ -267,6 +268,10 @@ export class IFrameWebview extends Disposable implements Webview { throw new Error('Method not implemented.'); } + runFindAction(previous: boolean): void { + throw new Error('Method not implemented.'); + } + public set state(state: string | undefined) { this.content = { html: this.content.html, diff --git a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index 0d8e6804bc067..b6c68c732bb67 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; export interface WebviewFindDelegate { find(value: string, previous: boolean): void; @@ -15,6 +16,7 @@ export interface WebviewFindDelegate { } export class WebviewFindWidget extends SimpleFindWidget { + protected _findWidgetFocused: IContextKey; constructor( private readonly _delegate: WebviewFindDelegate, @@ -22,6 +24,7 @@ export class WebviewFindWidget extends SimpleFindWidget { @IContextKeyService contextKeyService: IContextKeyService ) { super(contextViewService, contextKeyService); + this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); } public find(previous: boolean) { @@ -47,9 +50,13 @@ export class WebviewFindWidget extends SimpleFindWidget { return false; } - protected onFocusTrackerFocus() { } + protected onFocusTrackerFocus() { + this._findWidgetFocused.set(true); + } - protected onFocusTrackerBlur() { } + protected onFocusTrackerBlur() { + this._findWidgetFocused.reset(); + } protected onFindInputFocusTrackerFocus() { } diff --git a/src/vs/workbench/contrib/webview/browser/webviewService.ts b/src/vs/workbench/contrib/webview/browser/webviewService.ts index 44ef4a15f1e99..eed1445c49211 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewService.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement'; -import { IWebviewService, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { DynamicWebviewEditorOverlay } from './dynamicWebviewEditorOverlay'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/workbench/contrib/webview/common/portMapping.ts index 964c0ac6da057..e47390dcf0547 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/workbench/contrib/webview/common/portMapping.ts @@ -47,16 +47,16 @@ export class WebviewPortMappingManager extends Disposable { if (this.extensionLocation && this.extensionLocation.scheme === REMOTE_HOST_SCHEME) { const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort); if (tunnel) { - return uri.with({ + return encodeURI(uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}`, - }).toString(); + }).toString(true)); } } if (mapping.webviewPort !== mapping.extensionHostPort) { - return uri.with({ + return encodeURI(uri.with({ authority: `${requestLocalHostInfo.address}:${mapping.extensionHostPort}` - }).toString(); + }).toString(true)); } } } @@ -84,4 +84,4 @@ export class WebviewPortMappingManager extends Disposable { } return tunnel; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts index 084163f045916..627d57bd3fd24 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts @@ -13,7 +13,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; -import { IWebviewService, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/browser/webview'; import * as webviewCommands from 'vs/workbench/contrib/webview/electron-browser/webviewCommands'; import { ElectronWebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService'; @@ -89,4 +89,4 @@ function registerWebViewCommands(editorId: string): void { } } -registerWebViewCommands(WebviewEditor.ID); \ No newline at end of file +registerWebViewCommands(WebviewEditor.ID); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts index a1d8ecda01a83..066dbbecf5e36 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts @@ -9,7 +9,7 @@ import { Command, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; -import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/common/webview'; +import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; export class OpenWebviewDeveloperToolsAction extends Action { static readonly ID = 'workbench.action.webview.openDeveloperTools'; @@ -101,4 +101,4 @@ function withActiveWebviewBasedWebview(accessor: ServicesAccessor, f: (webview: } }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 3c030feb23544..ac65e4df36a3b 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -9,17 +9,18 @@ import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; -import { endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping'; import { getWebviewThemeData } from 'vs/workbench/contrib/webview/common/themeing'; -import { Webview, WebviewContentOptions, WebviewOptions, WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/webview'; +import { Webview, WebviewContentOptions, WebviewOptions, WebviewResourceScheme } from 'vs/workbench/contrib/webview/browser/webview'; import { registerFileProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols'; import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService'; import { WebviewFindWidget } from '../browser/webviewFindWidget'; @@ -135,51 +136,6 @@ class WebviewPortMappingProvider extends Disposable { } } -class SvgBlocker extends Disposable { - - private readonly _onDidBlockSvg = this._register(new Emitter()); - public readonly onDidBlockSvg = this._onDidBlockSvg.event; - - constructor( - session: WebviewSession, - private readonly _options: WebviewContentOptions, - ) { - super(); - - session.onBeforeRequest(async (details) => { - if (details.url.indexOf('.svg') > 0) { - const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && endsWith(uri.path, '.svg') && !this.isAllowedSvg(uri)) { - this._onDidBlockSvg.fire(); - return { cancel: true }; - } - } - - return undefined; - }); - - session.onHeadersReceived((details) => { - const headers: any = details.responseHeaders; - const contentType: string[] = headers['content-type'] || headers['Content-Type']; - if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { - const uri = URI.parse(details.url); - if (uri && !this.isAllowedSvg(uri)) { - this._onDidBlockSvg.fire(); - return { cancel: true }; - } - } - return undefined; - }); - } - - private isAllowedSvg(uri: URI): boolean { - if (this._options.svgWhiteList) { - return this._options.svgWhiteList.indexOf(uri.authority.toLowerCase()) >= 0; - } - return false; - } -} - class WebviewKeyboardHandler extends Disposable { private _ignoreMenuShortcut = false; @@ -284,6 +240,8 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { @IFileService fileService: IFileService, @ITunnelService tunnelService: ITunnelService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IEnvironmentService private readonly _environementService: IEnvironmentService, ) { super(); this.content = { @@ -331,11 +289,6 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { tunnelService, )); - if (!this._options.allowSvgs) { - const svgBlocker = this._register(new SvgBlocker(session, this.content.options)); - svgBlocker.onDidBlockSvg(() => this.onDidBlockSvg()); - } - this._register(new WebviewKeyboardHandler(this._webview)); this._register(addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) { @@ -366,7 +319,7 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { return; case 'did-click-link': - let [uri] = event.args; + const [uri] = event.args; this._onDidClickLink.fire(URI.parse(uri)); return; @@ -374,12 +327,17 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { { const rawEvent = event.args[0]; const bounds = this._webview.getBoundingClientRect(); - window.dispatchEvent(new MouseEvent(rawEvent.type, { - ...rawEvent, - clientX: rawEvent.clientX + bounds.left, - clientY: rawEvent.clientY + bounds.top, - })); - return; + try { + window.dispatchEvent(new MouseEvent(rawEvent.type, { + ...rawEvent, + clientX: rawEvent.clientX + bounds.left, + clientY: rawEvent.clientY + bounds.top, + })); + return; + } catch { + // CustomEvent was treated as MouseEvent so don't do anything - https://github.com/microsoft/vscode/issues/78915 + return; + } } case 'did-set-content': @@ -412,6 +370,10 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { case 'did-blur': this.handleFocusChange(false); return; + + case 'no-csp-found': + this.handleNoCspFound(); + return; } })); this._register(addDisposableListener(this._webview, 'devtools-opened', () => { @@ -532,7 +494,11 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { if (!this._webview) { return; } - this._webview.focus(); + try { + this._webview.focus(); + } catch { + // noop + } this._send('focus'); // Handle focus change programmatically (do not rely on event from ) @@ -546,14 +512,34 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { } } - public sendMessage(data: any): void { - this._send('message', data); + private _hasAlertedAboutMissingCsp = false; + + private handleNoCspFound(): void { + if (this._hasAlertedAboutMissingCsp) { + return; + } + this._hasAlertedAboutMissingCsp = true; + + if (this._options.extension && this._options.extension.id) { + if (this._environementService.isExtensionDevelopment) { + console.warn(`${this._options.extension.id.value} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`); + } + + type TelemetryClassification = { + extension?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' } + }; + type TelemetryData = { + extension?: string, + }; + + this._telemetryService.publicLog2('webviewMissingCsp', { + extension: this._options.extension.id.value + }); + } } - private onDidBlockSvg() { - this.sendMessage({ - name: 'vscode-did-block-svg' - }); + public sendMessage(data: any): void { + this._send('message', data); } private style(theme: ITheme): void { @@ -566,7 +552,7 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { } public layout(): void { - if (!this._webview) { + if (!this._webview || this._webview.style.width === '0px') { return; } const contents = this._webview.getWebContents(); @@ -652,6 +638,12 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { } } + public runFindAction(previous: boolean) { + if (this._webviewFindWidget) { + this._webviewFindWidget.find(previous); + } + } + public reload() { this.doUpdateContent(); } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts index ae8eb72d06fd3..dedb445ab5c0a 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts @@ -7,7 +7,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay'; import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement'; -import { IWebviewService, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; +import { IWebviewService, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; export class ElectronWebviewService implements IWebviewService { @@ -38,4 +38,4 @@ export class ElectronWebviewService implements IWebviewService { ): WebviewEditorOverlay { return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts index 2c0d6778d39c7..6bcad15f718c7 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.ts @@ -6,9 +6,11 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService, ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import * as platform from 'vs/base/common/platform'; import product from 'vs/platform/product/node/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export class GettingStarted implements IWorkbenchContribution { @@ -19,8 +21,9 @@ export class GettingStarted implements IWorkbenchContribution { constructor( @IStorageService private readonly storageService: IStorageService, - @IEnvironmentService environmentService: IEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { this.appName = product.nameLong; @@ -50,7 +53,7 @@ export class GettingStarted implements IWorkbenchContribution { if (platform.isLinux && platform.isRootUser()) { return; } - window.open(url); + this.openerService.open(URI.parse(url)); } private handleWelcome(): void { diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index ab0d4193c4049..f549552738b0b 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as strings from 'vs/base/common/strings'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as arrays from 'vs/base/common/arrays'; -import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput'; +import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts index 7a95ffda9e419..8493b87f2cdf9 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts @@ -8,7 +8,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Action } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { WalkThroughInput, WalkThroughInputOptions } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput'; +import { WalkThroughInput, WalkThroughInputOptions } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { Schemas } from 'vs/base/common/network'; import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md index 43e8bc83a893b..1f306f79ef216 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md @@ -5,7 +5,6 @@ The core editor in VS Code is packed with features. This page highlights a numb * [IntelliSense](#intellisense) - get code assistance and parameter suggestions for your code and external modules. * [Line Actions](#line-actions) - quickly move lines around to re-order your code. * [Rename Refactoring](#rename-refactoring) - quickly rename symbols across your code base. -* [Refactoring via Extraction](#refactoring-via-extraction) - quickly extract common code into a separate function or constant. * [Formatting](#formatting) - keep your code looking great with inbuilt document & selection formatting. * [Code Folding](#code-folding) - focus on the most relevant parts of your code by folding other areas. * [Errors and Warnings](#errors-and-warnings) - see errors and warning as you type. @@ -77,6 +76,9 @@ new Book("The Martian", "Andy Weir"); /** * Represents a book. + * + * @param {string} title Title of the book + * @param {string} author Who wrote the book */ function Book(title, author) { this.title = title; @@ -84,22 +86,7 @@ function Book(title, author) { } ``` -> **JSDoc Tip:** The example above also showcased another way to get IntelliSense hints by using `JSDoc` comments. You can try this out by invoking the `Book` function and seeing the enhanced context in the IntelliSense menu for the function as well as parameters. - - -### Refactoring via Extraction -Sometimes you want to refactor already written code into a separate function or constant to reuse it later. Select the lines you want to refactor out and press kb(editor.action.quickFix) or click the little light bulb and choose one of the respective `Extract to...` options. Try it by selecting the code inside the `if`-clause on line 3 or any other common code you want to refactor out. - -```js -function findFirstEvenNumber(arr) { - for (const el of arr) { - if (typeof el === 'number' && el % 2 === 0) { - return el; - } - } - return null; -} -``` +> **JSDoc Tip:** VS Code's IntelliSense uses JSDoc comments to provide richer suggestions. The types and documentation from JSDoc comments show up when you hover over a reference to `Book` or in IntelliSense when you create a new instance of `Book`. ### Formatting diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts index 9eed29a774f4c..8a91091c6b706 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput'; +import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { WalkThroughPart } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart'; import { WalkThroughArrowUp, WalkThroughArrowDown, WalkThroughPageUp, WalkThroughPageDown } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughActions'; import { WalkThroughContentProvider, WalkThroughSnippetContentProvider } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider'; @@ -55,4 +55,4 @@ MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { title: localize({ key: 'miInteractivePlayground', comment: ['&& denotes a mnemonic'] }, "I&&nteractive Playground") }, order: 2 -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts similarity index 100% rename from src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput.ts rename to src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index afd74dc859ba9..71faece75fd57 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -12,7 +12,7 @@ import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/com 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'; -import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput'; +import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import * as marked from 'vs/base/common/marked/marked'; import { IModelService } from 'vs/editor/common/services/modelService'; diff --git a/src/vs/workbench/electron-browser/actions/helpActions.ts b/src/vs/workbench/electron-browser/actions/helpActions.ts index 3c5d242f92f16..ce5e237ef08c9 100644 --- a/src/vs/workbench/electron-browser/actions/helpActions.ts +++ b/src/vs/workbench/electron-browser/actions/helpActions.ts @@ -8,6 +8,8 @@ import * as nls from 'vs/nls'; import product from 'vs/platform/product/node/product'; import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export class KeybindingsReferenceAction extends Action { @@ -19,13 +21,14 @@ export class KeybindingsReferenceAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(KeybindingsReferenceAction.URL); + this.openerService.open(URI.parse(KeybindingsReferenceAction.URL)); return Promise.resolve(); } @@ -41,13 +44,14 @@ export class OpenDocumentationUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(OpenDocumentationUrlAction.URL); + this.openerService.open(URI.parse(OpenDocumentationUrlAction.URL)); return Promise.resolve(); } @@ -63,13 +67,14 @@ export class OpenIntroductoryVideosUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(OpenIntroductoryVideosUrlAction.URL); + this.openerService.open(URI.parse(OpenIntroductoryVideosUrlAction.URL)); return Promise.resolve(); } @@ -85,13 +90,14 @@ export class OpenTipsAndTricksUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { - window.open(OpenTipsAndTricksUrlAction.URL); + this.openerService.open(URI.parse(OpenTipsAndTricksUrlAction.URL)); return Promise.resolve(); } } @@ -107,6 +113,7 @@ export class OpenNewsletterSignupUrlAction extends Action { constructor( id: string, label: string, + @IOpenerService private readonly openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService ) { super(id, label); @@ -116,7 +123,7 @@ export class OpenNewsletterSignupUrlAction extends Action { async run(): Promise { const info = await this.telemetryService.getTelemetryInfo(); - window.open(`${OpenNewsletterSignupUrlAction.URL}?machineId=${encodeURIComponent(info.machineId)}`); + this.openerService.open(URI.parse(`${OpenNewsletterSignupUrlAction.URL}?machineId=${encodeURIComponent(info.machineId)}`)); } } @@ -127,14 +134,15 @@ export class OpenTwitterUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { if (product.twitterUrl) { - window.open(product.twitterUrl); + this.openerService.open(URI.parse(product.twitterUrl)); } return Promise.resolve(); @@ -148,14 +156,15 @@ export class OpenRequestFeatureUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } run(): Promise { if (product.requestFeatureUrl) { - window.open(product.requestFeatureUrl); + this.openerService.open(URI.parse(product.requestFeatureUrl)); } return Promise.resolve(); @@ -169,7 +178,8 @@ export class OpenLicenseUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } @@ -178,9 +188,9 @@ export class OpenLicenseUrlAction extends Action { if (product.licenseUrl) { if (language) { const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; - window.open(`${product.licenseUrl}${queryArgChar}lang=${language}`); + this.openerService.open(URI.parse(`${product.licenseUrl}${queryArgChar}lang=${language}`)); } else { - window.open(product.licenseUrl); + this.openerService.open(URI.parse(product.licenseUrl)); } } @@ -195,7 +205,8 @@ export class OpenPrivacyStatementUrlAction extends Action { constructor( id: string, - label: string + label: string, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } @@ -204,9 +215,9 @@ export class OpenPrivacyStatementUrlAction extends Action { if (product.privacyStatementUrl) { if (language) { const queryArgChar = product.privacyStatementUrl.indexOf('?') > 0 ? '&' : '?'; - window.open(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`); + this.openerService.open(URI.parse(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`)); } else { - window.open(product.privacyStatementUrl); + this.openerService.open(URI.parse(product.privacyStatementUrl)); } } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts similarity index 100% rename from src/vs/workbench/electron-browser/main.contribution.ts rename to src/vs/workbench/electron-browser/desktop.contribution.ts diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/desktop.main.ts similarity index 98% rename from src/vs/workbench/electron-browser/main.ts rename to src/vs/workbench/electron-browser/desktop.main.ts index 40ea992a2a152..0d72bef3b4288 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -51,6 +51,8 @@ import { SignService } from 'vs/platform/sign/node/signService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { basename } from 'vs/base/common/resources'; +import { IProductService } from 'vs/platform/product/common/product'; +import product from 'vs/platform/product/node/product'; class CodeRendererMain extends Disposable { @@ -177,6 +179,9 @@ class CodeRendererMain extends Disposable { // Environment serviceCollection.set(IWorkbenchEnvironmentService, this.environmentService); + // Product + serviceCollection.set(IProductService, { _serviceBrand: undefined, ...product }); + // Log const logService = this._register(this.createLogService(mainProcessService, this.environmentService)); serviceCollection.set(ILogService, logService); @@ -189,7 +194,7 @@ class CodeRendererMain extends Disposable { const signService = new SignService(); serviceCollection.set(ISignService, signService); - const remoteAgentService = this._register(new RemoteAgentService(this.environmentService.configuration, this.environmentService, remoteAuthorityResolverService, signService)); + const remoteAgentService = this._register(new RemoteAgentService(this.environmentService.configuration, this.environmentService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); // Files diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index bfc7916011e5d..7f13270ca830b 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -14,7 +14,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IRunKeybindingInWindowRequest } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IRunKeybindingInWindowRequest, getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -24,7 +24,7 @@ import { IResourceInput } from 'vs/platform/editor/common/editor'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { ipcRenderer as ipc, webFrame, crashReporter, Event } from 'electron'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; -import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -32,7 +32,7 @@ import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecyc import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; -import { isRootUser, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; +import { isRootUser, isWindows, isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; import product from 'vs/platform/product/node/product'; import pkg from 'vs/platform/product/node/package'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -45,6 +45,17 @@ import { coalesce } from 'vs/base/common/arrays'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isEqual } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MenubarControl } from '../browser/parts/titlebar/menubarControl'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IUpdateService } from 'vs/platform/update/common/update'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IPreferencesService } from '../services/preferences/common/preferences'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IMenubarService, IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/node/menubar'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Schemas } from 'vs/base/common/network'; const TextInputActions: IAction[] = [ new Action('undo', nls.localize('undo', "Undo"), undefined, true, () => Promise.resolve(document.execCommand('undo'))), @@ -91,7 +102,9 @@ export class ElectronWindow extends Disposable { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ITextFileService private readonly textFileService: ITextFileService + @ITextFileService private readonly textFileService: ITextFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); @@ -297,13 +310,13 @@ export class ElectronWindow extends Disposable { private create(): void { - // Handle window.open() calls - const $this = this; - window.open = function (url: string, target: string, features: string, replace: boolean): Window | null { - $this.windowsService.openExternal(url); + // Native menu controller + if (isMacintosh || getTitleBarStyle(this.configurationService, this.environmentService) === 'native') { + this._register(this.instantiationService.createInstance(NativeMenubarControl)); + } - return null; - }; + // Handle open calls + this.setupOpenHandlers(); // Emit event when vscode is ready this.lifecycleService.when(LifecyclePhase.Ready).then(() => ipc.send('vscode:workbenchReady', this.windowService.windowId)); @@ -341,6 +354,39 @@ export class ElectronWindow extends Disposable { } } + private setupOpenHandlers(): void { + + // Block window.open() calls + const $this = this; + window.open = function (): Window | null { + throw new Error('Prevented call to window.open(). Use IOpenerService instead!'); + }; + + // Handle internal open() calls + this.openerService.registerOpener({ + async open(resource: URI, options?: { openToSide?: boolean; openExternal?: boolean; } | undefined): Promise { + + // If either the caller wants to open externally or the + // scheme is one where we prefer to open externally + // we handle this resource by delegating the opening to + // the main process to prevent window focus issues. + const scheme = resource.scheme.toLowerCase(); + const preferOpenExternal = (scheme === Schemas.mailto || scheme === Schemas.http || scheme === Schemas.https); + if ((options && options.openExternal) || preferOpenExternal) { + const success = await $this.windowsService.openExternal(encodeURI(resource.toString(true))); + if (!success && resource.scheme === Schemas.file) { + // if opening failed, and this is a file, we can still try to reveal it + await $this.windowsService.showItemInFolder(resource); + } + + return true; + } + + return false; // not handled by us + } + }); + } + private updateTouchbarMenu(): void { if (!isMacintosh) { return; // macOS only @@ -538,3 +584,192 @@ export class ElectronWindow extends Disposable { return this.editorService.openEditors(resources); } } + +class NativeMenubarControl extends MenubarControl { + constructor( + @IMenuService menuService: IMenuService, + @IWindowService windowService: IWindowService, + @IWindowsService windowsService: IWindowsService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService, + @ILabelService labelService: ILabelService, + @IUpdateService updateService: IUpdateService, + @IStorageService storageService: IStorageService, + @INotificationService notificationService: INotificationService, + @IPreferencesService preferencesService: IPreferencesService, + @IEnvironmentService environmentService: IEnvironmentService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IMenubarService private readonly menubarService: IMenubarService + ) { + super( + menuService, + windowService, + windowsService, + contextKeyService, + keybindingService, + configurationService, + labelService, + updateService, + storageService, + notificationService, + preferencesService, + environmentService, + accessibilityService); + + if (isMacintosh && !isWeb) { + this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService)); + this.topLevelTitles['Preferences'] = nls.localize('mPreferences', "Preferences"); + } + + for (const topLevelMenuName of Object.keys(this.topLevelTitles)) { + const menu = this.menus[topLevelMenuName]; + if (menu) { + this._register(menu.onDidChange(() => this.updateMenubar())); + } + } + + this.windowService.getRecentlyOpened().then((recentlyOpened) => { + this.recentlyOpened = recentlyOpened; + + this.doUpdateMenubar(true); + }); + + this.registerListeners(); + } + + protected doUpdateMenubar(firstTime: boolean): void { + + // Send menus to main process to be rendered by Electron + const menubarData = { menus: {}, keybindings: {} }; + if (this.getMenubarMenus(menubarData)) { + this.menubarService.updateMenubar(this.windowService.windowId, menubarData); + } + } + + private getMenubarMenus(menubarData: IMenubarData): boolean { + if (!menubarData) { + return false; + } + + menubarData.keybindings = this.getAdditionalKeybindings(); + for (const topLevelMenuName of Object.keys(this.topLevelTitles)) { + const menu = this.menus[topLevelMenuName]; + if (menu) { + const menubarMenu: IMenubarMenu = { items: [] }; + this.populateMenuItems(menu, menubarMenu, menubarData.keybindings); + if (menubarMenu.items.length === 0) { + return false; // Menus are incomplete + } + menubarData.menus[topLevelMenuName] = menubarMenu; + } + } + + return true; + } + + private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding | undefined }) { + let groups = menu.getActions(); + for (let group of groups) { + const [, actions] = group; + + actions.forEach(menuItem => { + + if (menuItem instanceof SubmenuItemAction) { + const submenu = { items: [] }; + + if (!this.menus[menuItem.item.submenu]) { + this.menus[menuItem.item.submenu] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService); + this._register(this.menus[menuItem.item.submenu]!.onDidChange(() => this.updateMenubar())); + } + + const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService); + this.populateMenuItems(menuToDispose, submenu, keybindings); + + let menubarSubmenuItem: IMenubarMenuItemSubmenu = { + id: menuItem.id, + label: menuItem.label, + submenu: submenu + }; + + menuToPopulate.items.push(menubarSubmenuItem); + menuToDispose.dispose(); + } else { + if (menuItem.id === 'workbench.action.openRecent') { + const actions = this.getOpenRecentActions().map(this.transformOpenRecentAction); + menuToPopulate.items.push(...actions); + } + + let menubarMenuItem: IMenubarMenuItemAction = { + id: menuItem.id, + label: menuItem.label + }; + + if (menuItem.checked) { + menubarMenuItem.checked = true; + } + + if (!menuItem.enabled) { + menubarMenuItem.enabled = false; + } + + menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem); + keybindings[menuItem.id] = this.getMenubarKeybinding(menuItem.id); + menuToPopulate.items.push(menubarMenuItem); + } + }); + + menuToPopulate.items.push({ id: 'vscode.menubar.separator' }); + } + + if (menuToPopulate.items.length > 0) { + menuToPopulate.items.pop(); + } + } + + private transformOpenRecentAction(action: Separator | (IAction & { uri: URI })): MenubarMenuItem { + if (action instanceof Separator) { + return { id: 'vscode.menubar.separator' }; + } + + return { + id: action.id, + uri: action.uri, + enabled: action.enabled, + label: action.label + }; + } + + private getAdditionalKeybindings(): { [id: string]: IMenubarKeybinding } { + const keybindings: { [id: string]: IMenubarKeybinding } = {}; + if (isMacintosh) { + const keybinding = this.getMenubarKeybinding('workbench.action.quit'); + if (keybinding) { + keybindings['workbench.action.quit'] = keybinding; + } + } + + return keybindings; + } + + private getMenubarKeybinding(id: string): IMenubarKeybinding | undefined { + const binding = this.keybindingService.lookupKeybinding(id); + if (!binding) { + return undefined; + } + + // first try to resolve a native accelerator + const electronAccelerator = binding.getElectronAccelerator(); + if (electronAccelerator) { + return { label: electronAccelerator, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) }; + } + + // we need this fallback to support keybindings that cannot show in electron menus (e.g. chords) + const acceleratorLabel = binding.getLabel(); + if (acceleratorLabel) { + return { label: acceleratorLabel, isNative: false, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) }; + } + + return undefined; + } +} diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 170e3f136aa02..a5f15850dbf75 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -17,9 +17,9 @@ export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; -export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; -export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; -export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE]; +export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.MACHINE_OVERRIDABLE]; +export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.MACHINE_OVERRIDABLE]; +export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const TASKS_CONFIGURATION_KEY = 'tasks'; export const LAUNCH_CONFIGURATION_KEY = 'launch'; @@ -36,4 +36,4 @@ export interface IConfigurationCache { write(key: ConfigurationKey, content: string): Promise; remove(key: ConfigurationKey): Promise; -} \ No newline at end of file +} 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 b6e9d5d09fa7a..23122b98674b7 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 @@ -109,7 +109,7 @@ suite('WorkspaceContextService - Folder', () => { const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, new DiskFileSystemProvider(new NullLogService()), environmentService)); - workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService({}, environmentService, new RemoteAuthorityResolverService(), new SignService(undefined))); + workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService({}, environmentService, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService())); return (workspaceContextService).initialize(convertToWorkspacePayload(URI.file(folderDir))); }); }); diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 4709a15555b50..f5d0b60b07b67 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -55,7 +55,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR if (activeEditor instanceof DiffEditorInput) { activeEditor = activeEditor.modifiedInput; } - const fileResource = toResource(activeEditor, { filterByScheme: Schemas.file }); + const fileResource = toResource(activeEditor, { filterByScheme: [Schemas.file, Schemas.userData] }); if (!fileResource) { return undefined; } 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 dd668c74f6d07..66236f88de864 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 @@ -20,7 +20,6 @@ import * as Types from 'vs/base/common/types'; import { EditorType } from 'vs/editor/common/editorCommon'; import { Selection } from 'vs/editor/common/core/selection'; import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; -import { parseArgs } from 'vs/platform/environment/node/argv'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -632,11 +631,7 @@ class MockInputsConfigurationService extends TestConfigurationService { class MockWorkbenchEnvironmentService extends WorkbenchEnvironmentService { - constructor(private env: platform.IProcessEnvironment) { - super(parseArgs(process.argv) as IWindowConfiguration, process.execPath); - } - - get configuration(): IWindowConfiguration { - return { userEnv: this.env } as IWindowConfiguration; + constructor(env: platform.IProcessEnvironment) { + super({ userEnv: env } as IWindowConfiguration, process.execPath); } } diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts new file mode 100644 index 0000000000000..b7613b9c23c1e --- /dev/null +++ b/src/vs/workbench/services/credentials/browser/credentialsService.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. + *--------------------------------------------------------------------------------------------*/ + +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export interface ICredentialsProvider { + getPassword(service: string, account: string): Promise; + setPassword(service: string, account: string, password: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; +} + +export class BrowserCredentialsService implements ICredentialsService { + + _serviceBrand!: ServiceIdentifier; + + private credentialsProvider: ICredentialsProvider; + + constructor(@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService) { + if (environmentService.options && environmentService.options.credentialsProvider) { + this.credentialsProvider = environmentService.options.credentialsProvider; + } else { + this.credentialsProvider = new LocalStorageCredentialsProvider(); + } + } + + async getPassword(service: string, account: string): Promise { + return this.credentialsProvider.getPassword(service, account); + } + + async setPassword(service: string, account: string, password: string): Promise { + return this.credentialsProvider.setPassword(service, account, password); + } + + async deletePassword(service: string, account: string): Promise { + return this.credentialsProvider.deletePassword(service, account); + } + + async findPassword(service: string): Promise { + return this.credentialsProvider.findPassword(service); + } +} + +interface ICredential { + service: string; + account: string; + password: string; +} + +class LocalStorageCredentialsProvider implements ICredentialsProvider { + + static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider'; + + private _credentials: ICredential[]; + private get credentials(): ICredential[] { + if (!this._credentials) { + try { + const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); + if (serializedCredentials) { + this._credentials = JSON.parse(serializedCredentials); + } + } catch (error) { + // ignore + } + + if (!Array.isArray(this._credentials)) { + this._credentials = []; + } + } + + return this._credentials; + } + + private save(): void { + window.localStorage.setItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY, JSON.stringify(this.credentials)); + } + + async getPassword(service: string, account: string): Promise { + return this.doGetPassword(service, account); + } + + private async doGetPassword(service: string, account?: string): Promise { + for (const credential of this.credentials) { + if (credential.service === service) { + if (typeof account !== 'string' || account === credential.account) { + return credential.password; + } + } + } + + return null; + } + + async setPassword(service: string, account: string, password: string): Promise { + this.deletePassword(service, account); + + this.credentials.push({ service, account, password }); + + this.save(); + } + + async deletePassword(service: string, account: string): Promise { + let found = false; + + this._credentials = this.credentials.filter(credential => { + if (credential.service === service && credential.account === account) { + found = true; + + return false; + } + + return true; + }); + + if (found) { + this.save(); + } + + return found; + } + + async findPassword(service: string): Promise { + return this.doGetPassword(service); + } +} + +registerSingleton(ICredentialsService, BrowserCredentialsService, true); diff --git a/src/vs/platform/credentials/common/credentials.ts b/src/vs/workbench/services/credentials/common/credentials.ts similarity index 84% rename from src/vs/platform/credentials/common/credentials.ts rename to src/vs/workbench/services/credentials/common/credentials.ts index dc6618d89f780..8fa520c374e74 100644 --- a/src/vs/platform/credentials/common/credentials.ts +++ b/src/vs/workbench/services/credentials/common/credentials.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export const ICredentialsService = createDecorator('ICredentialsService'); export interface ICredentialsService { - _serviceBrand: any; + + _serviceBrand: ServiceIdentifier; + getPassword(service: string, account: string): Promise; setPassword(service: string, account: string, password: string): Promise; deletePassword(service: string, account: string): Promise; diff --git a/src/vs/platform/credentials/node/credentialsService.ts b/src/vs/workbench/services/credentials/node/credentialsService.ts similarity index 80% rename from src/vs/platform/credentials/node/credentialsService.ts rename to src/vs/workbench/services/credentials/node/credentialsService.ts index 282b761fe2e65..1361f169be39f 100644 --- a/src/vs/platform/credentials/node/credentialsService.ts +++ b/src/vs/workbench/services/credentials/node/credentialsService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; import { IdleValue } from 'vs/base/common/async'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; type KeytarModule = { getPassword(service: string, account: string): Promise; @@ -15,7 +17,7 @@ type KeytarModule = { export class KeytarCredentialsService implements ICredentialsService { - _serviceBrand: any; + _serviceBrand!: ServiceIdentifier; private readonly _keytar = new IdleValue>(() => import('keytar')); @@ -39,3 +41,5 @@ export class KeytarCredentialsService implements ICredentialsService { return keytar.findPassword(service); } } + +registerSingleton(ICredentialsService, KeytarCredentialsService, true); diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 3bd6b03f963d2..7842223793642 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -19,7 +19,7 @@ import { basename } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { localize } from 'vs/nls'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditor, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { coalesce } from 'vs/base/common/arrays'; @@ -29,13 +29,14 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; -type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; +type CachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; +type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; export class EditorService extends Disposable implements EditorServiceImpl { _serviceBrand!: ServiceIdentifier; - private static CACHE: ResourceMap = new ResourceMap(); + private static CACHE: ResourceMap = new ResourceMap(); //#region events @@ -216,11 +217,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region openEditor() - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: GroupIdentifier): Promise { + openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise; + openEditor(editor: IResourceInput | IUntitledResourceInput, group?: OpenInEditorGroup): Promise; + openEditor(editor: IResourceDiffInput, group?: OpenInEditorGroup): Promise; + openEditor(editor: IResourceSideBySideInput, group?: OpenInEditorGroup): Promise; + async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { // Typed Editor Support if (editor instanceof EditorInput) { @@ -240,14 +241,14 @@ export class EditorService extends Disposable implements EditorServiceImpl { return this.doOpenEditor(targetGroup, typedInput, editorOptions); } - return Promise.resolve(null); + return undefined; } - protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise { - return group.openEditor(editor, options); + protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise { + return withNullAsUndefined(await group.openEditor(editor, options)); } - private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): IEditorGroup { + private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): IEditorGroup { let targetGroup: IEditorGroup | undefined; // Group: Instance of Group @@ -272,7 +273,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Respect option to reveal an editor if it is already visible in any group if (options && options.revealIfVisible) { for (const group of groupsByLastActive) { - if (input.matches(group.activeEditor)) { + if (group.isActive(input)) { targetGroup = group; break; } @@ -280,12 +281,30 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Respect option to reveal an editor if it is open (not necessarily visible) - if ((options && options.revealIfOpened) || this.configurationService.getValue('workbench.editor.revealIfOpen')) { - for (const group of groupsByLastActive) { - if (group.isOpened(input)) { - targetGroup = group; - break; + // Still prefer to reveal an editor in a group where the editor is active though. + if (!targetGroup) { + if ((options && options.revealIfOpened) || this.configurationService.getValue('workbench.editor.revealIfOpen')) { + let groupWithInputActive: IEditorGroup | undefined = undefined; + let groupWithInputOpened: IEditorGroup | undefined = undefined; + + for (const group of groupsByLastActive) { + if (group.isOpened(input)) { + if (!groupWithInputOpened) { + groupWithInputOpened = group; + } + + if (!groupWithInputActive && group.isActive(input)) { + groupWithInputActive = group; + } + } + + if (groupWithInputOpened && groupWithInputActive) { + break; // we found all groups we wanted + } } + + // Prefer a target group where the input is visible + targetGroup = groupWithInputActive || groupWithInputOpened; } } } @@ -326,9 +345,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region openEditors() - openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - async openEditors(editors: Array, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise { + openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; + openEditors(editors: IResourceEditor[], group?: OpenInEditorGroup): Promise; + async openEditors(editors: Array, group?: OpenInEditorGroup): Promise { // Convert to typed editors and options const typedEditors: IEditorInputWithOptions[] = []; @@ -373,7 +392,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region isOpen() - isOpen(editor: IEditorInput | IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): boolean { + isOpen(editor: IEditorInput | IResourceInput | IUntitledResourceInput): boolean { return !!this.doGetOpened(editor); } @@ -381,11 +400,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region getOpend() - getOpened(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): IEditorInput | undefined { + getOpened(editor: IResourceInput | IUntitledResourceInput): IEditorInput | undefined { return this.doGetOpened(editor); } - private doGetOpened(editor: IEditorInput | IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): IEditorInput | undefined { + private doGetOpened(editor: IEditorInput | IResourceInput | IUntitledResourceInput): IEditorInput | undefined { if (!(editor instanceof EditorInput)) { const resourceInput = editor as IResourceInput | IUntitledResourceInput; if (!resourceInput.resource) { @@ -393,20 +412,8 @@ export class EditorService extends Disposable implements EditorServiceImpl { } } - let groups: IEditorGroup[] = []; - if (typeof group === 'number') { - const groupView = this.editorGroupService.getGroup(group); - if (groupView) { - groups.push(groupView); - } - } else if (group) { - groups.push(group); - } else { - groups = [...this.editorGroupService.groups]; - } - // For each editor group - for (const group of groups) { + for (const group of this.editorGroupService.groups) { // Typed editor if (editor instanceof EditorInput) { @@ -548,7 +555,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { throw new Error('Unknown input type'); } - private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, mode: string | undefined, forceFile: boolean | undefined): ICachedEditorInput { + private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, mode: string | undefined, forceFile: boolean | undefined): CachedEditorInput { if (EditorService.CACHE.has(resource)) { const input = EditorService.CACHE.get(resource)!; if (input instanceof ResourceEditorInput) { @@ -576,9 +583,8 @@ export class EditorService extends Disposable implements EditorServiceImpl { return input; } - let input: ICachedEditorInput; - // File + let input: CachedEditorInput; if (forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resource)) { input = this.fileInputFactory.createFileInput(resource, encoding, mode, instantiationService); } @@ -651,7 +657,7 @@ export class DelegatingEditorService extends EditorService { this.editorOpenHandler = handler; } - protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise { + protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise { if (!this.editorOpenHandler) { return super.doOpenEditor(group, editor, options); } diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index ab79c65ff489e..5664c9c8c4530 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -36,7 +36,7 @@ export interface IOpenEditorOverride { * If defined, will prevent the opening of an editor and replace the resulting * promise with the provided promise for the openEditor() call. */ - override?: Promise; + override?: Promise; } export interface IVisibleEditor extends IEditor { @@ -117,13 +117,13 @@ export interface IEditorService { * active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side * of the currently active group. * - * @returns the editor that opened or NULL if the operation failed or the editor was not + * @returns the editor that opened or `undefined` if the operation failed or the editor was not * opened to be active. */ - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; /** * Open editors in an editor group. @@ -185,4 +185,4 @@ export interface IEditorService { * Converts a lightweight input to a workbench editor input. */ createInput(input: IResourceEditor): IEditorInput | null; -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 250fd43582e24..7c3b6ae53e0a5 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowConfiguration, IPath, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; -import { IEnvironmentService, IExtensionHostDebugParams, IDebugParams, BACKUPS } from 'vs/platform/environment/common/environment'; +import { IExtensionHostDebugParams, IDebugParams, BACKUPS } from 'vs/platform/environment/common/environment'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -13,6 +13,9 @@ import { ExportData } from 'vs/base/common/performance'; import { LogLevel } from 'vs/platform/log/common/log'; import { joinPath } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; +import { generateUuid } from 'vs/base/common/uuid'; export class BrowserWindowConfiguration implements IWindowConfiguration { @@ -59,46 +62,43 @@ export class BrowserWindowConfiguration implements IWindowConfiguration { termProgram?: string; } -export interface IBrowserWindowConfiguration { +interface IBrowserWorkbenchEnvironemntConstructionOptions extends IWorkbenchConstructionOptions { workspaceId: string; - remoteAuthority?: string; - webviewEndpoint?: string; - connectionToken?: string; + logsPath: URI; } -export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { +export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironmentService { - _serviceBrand!: ServiceIdentifier; + _serviceBrand!: ServiceIdentifier; readonly configuration: IWindowConfiguration = new BrowserWindowConfiguration(); - constructor(configuration: IBrowserWindowConfiguration) { + constructor(readonly options: IBrowserWorkbenchEnvironemntConstructionOptions) { this.args = { _: [] }; + this.logsPath = options.logsPath.path; + this.logFile = joinPath(options.logsPath, 'window.log'); this.appRoot = '/web/'; this.appNameLong = 'Visual Studio Code - Web'; - this.configuration.remoteAuthority = configuration.remoteAuthority; + this.configuration.remoteAuthority = options.remoteAuthority; + this.configuration.machineId = generateUuid(); this.userRoamingDataHome = URI.file('/User').with({ scheme: Schemas.userData }); this.settingsResource = joinPath(this.userRoamingDataHome, 'settings.json'); this.keybindingsResource = joinPath(this.userRoamingDataHome, 'keybindings.json'); this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); this.localeResource = joinPath(this.userRoamingDataHome, 'locale.json'); this.backupHome = joinPath(this.userRoamingDataHome, BACKUPS); - this.configuration.backupWorkspaceResource = joinPath(this.backupHome, configuration.workspaceId); - this.configuration.connectionToken = configuration.connectionToken || this.getConnectionTokenFromLocation(); - - this.logsPath = '/web/logs'; + this.configuration.backupWorkspaceResource = joinPath(this.backupHome, options.workspaceId); + this.configuration.connectionToken = options.connectionToken || getCookieValue('vscode-tkn'); this.debugExtensionHost = { port: null, break: false }; - this.webviewEndpoint = configuration.webviewEndpoint; this.untitledWorkspacesHome = URI.from({ scheme: Schemas.untitled, path: 'Workspaces' }); if (document && document.location && document.location.search) { - const map = new Map(); const query = document.location.search.substring(1); const vars = query.split('&'); @@ -168,7 +168,6 @@ export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { verbose: boolean; skipGettingStarted: boolean; skipReleaseNotes: boolean; - skipAddToRecentlyOpened: boolean; mainIPCHandle: string; sharedIPCHandle: string; nodeCachedDataDir?: string; @@ -177,31 +176,22 @@ export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { disableCrashReporter: boolean; driverHandle?: string; driverVerbose: boolean; - webviewEndpoint?: string; galleryMachineIdResource?: URI; + readonly logFile: URI; get webviewResourceRoot(): string { - return this.webviewEndpoint ? this.webviewEndpoint + '/vscode-resource{{resource}}' : 'vscode-resource:{{resource}}'; + return this.options.webviewEndpoint ? `${this.options.webviewEndpoint}/vscode-resource{{resource}}` : 'vscode-resource:{{resource}}'; } get webviewCspSource(): string { - return this.webviewEndpoint ? this.webviewEndpoint : 'vscode-resource:'; - } - - private getConnectionTokenFromLocation(): string | undefined { - // TODO: Check with @alexd where the token will be: search or hash? - let connectionToken: string | undefined = undefined; - if (document.location.search) { - connectionToken = this.getConnectionToken(document.location.search); - } - if (!connectionToken && document.location.hash) { - connectionToken = this.getConnectionToken(document.location.hash); - } - return connectionToken; + return this.options.webviewEndpoint ? this.options.webviewEndpoint : 'vscode-resource:'; } +} - private getConnectionToken(str: string): string | undefined { - const m = str.match(/[#&]tkn=([^&]+)/); - return m ? m[1] : undefined; - } +/** + * See https://stackoverflow.com/a/25490531 + */ +function getCookieValue(name: string): string | undefined { + const m = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); + return m ? m.pop() : undefined; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index fd4beaf134d76..4ed892d7b0277 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -5,7 +5,9 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IEnvironmentService, IDebugParams } from 'vs/platform/environment/common/environment'; +import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; +import { URI } from 'vs/base/common/uri'; export const IWorkbenchEnvironmentService = createDecorator('environmentService'); @@ -14,4 +16,18 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { _serviceBrand: ServiceIdentifier; readonly configuration: IWindowConfiguration; + + readonly options?: IWorkbenchConstructionOptions; + + readonly logFile: URI; + readonly logExtensionHostCommunication: boolean; + + readonly debugSearch: IDebugParams; + + readonly webviewResourceRoot: string; + readonly webviewCspSource: string; + + readonly skipGettingStarted: boolean | undefined; + readonly skipReleaseNotes: boolean | undefined; + } diff --git a/src/vs/workbench/services/environment/node/environmentService.ts b/src/vs/workbench/services/environment/node/environmentService.ts index 2ebf10365a2ec..bedcfdc999524 100644 --- a/src/vs/workbench/services/environment/node/environmentService.ts +++ b/src/vs/workbench/services/environment/node/environmentService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { EnvironmentService, parseSearchPort } from 'vs/platform/environment/node/environmentService'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { memoize } from 'vs/base/common/decorators'; @@ -11,24 +11,37 @@ import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { join } from 'vs/base/common/path'; +import { IDebugParams } from 'vs/platform/environment/common/environment'; export class WorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService { _serviceBrand!: ServiceIdentifier; + readonly webviewResourceRoot = 'vscode-resource:{{resource}}'; + readonly webviewCspSource = 'vscode-resource:'; + constructor( - private _configuration: IWindowConfiguration, + readonly configuration: IWindowConfiguration, execPath: string ) { - super(_configuration, execPath); + super(configuration, execPath); - this._configuration.backupWorkspaceResource = this._configuration.backupPath ? toBackupWorkspaceResource(this._configuration.backupPath, this) : undefined; + this.configuration.backupWorkspaceResource = this.configuration.backupPath ? toBackupWorkspaceResource(this.configuration.backupPath, this) : undefined; } - get configuration(): IWindowConfiguration { - return this._configuration; - } + get skipGettingStarted(): boolean { return !!this.args['skip-getting-started']; } + + get skipReleaseNotes(): boolean { return !!this.args['skip-release-notes']; } @memoize get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); } + + @memoize + get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.configuration.windowId}.log`)); } + + get logExtensionHostCommunication(): boolean { return !!this.args.logExtensionHostCommunication; } + + @memoize + get debugSearch(): IDebugParams { return parseSearchPort(this.args, this.isBuilt); } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 337f52b278a1c..f1831bdbc19b4 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -8,7 +8,7 @@ import { IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionType, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -17,6 +17,8 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { localize } from 'vs/nls'; import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IProductService } from 'vs/platform/product/common/product'; +import { Schemas } from 'vs/base/common/network'; +import { IDownloadService } from 'vs/platform/download/common/download'; export class ExtensionManagementService extends Disposable implements IExtensionManagementService { @@ -34,6 +36,7 @@ export class ExtensionManagementService extends Disposable implements IExtension @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService protected readonly configurationService: IConfigurationService, @IProductService protected readonly productService: IProductService, + @IDownloadService protected readonly downloadService: IDownloadService, ) { super(); if (this.extensionManagementServerService.localExtensionManagementServer) { @@ -142,15 +145,43 @@ export class ExtensionManagementService extends Disposable implements IExtension } async install(vsix: URI): Promise { + if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { + const manifest = await this.getManifest(vsix); + if (isLanguagePackExtension(manifest)) { + // Install on both servers + const [local] = await Promise.all(this.servers.map(server => this.installVSIX(vsix, server))); + return local; + } + if (isUIExtension(manifest, this.productService, this.configurationService)) { + // Install only on local server + return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); + } + // Install only on remote server + return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer); + } if (this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix); + return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); } if (this.extensionManagementServerService.remoteExtensionManagementServer) { - return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.install(vsix); + return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer); } return Promise.reject('No Servers to Install'); } + protected installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { + return server.extensionManagementService.install(vsix); + } + + getManifest(vsix: URI): Promise { + if (vsix.scheme === Schemas.file && this.extensionManagementServerService.localExtensionManagementServer) { + return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getManifest(vsix); + } + if (vsix.scheme === Schemas.vscodeRemote && this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.getManifest(vsix); + } + return Promise.reject('No Servers'); + } + async installFromGallery(gallery: IGalleryExtension): Promise { if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); @@ -191,4 +222,4 @@ export class ExtensionManagementService extends Disposable implements IExtension private getServer(extension: ILocalExtension): IExtensionManagementServer | null { return this.extensionManagementServerService.getExtensionManagementServer(extension.location); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/extensionManagement/node/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/node/extensionManagementService.ts index b2b5e78f90e09..143026c2b91e9 100644 --- a/src/vs/workbench/services/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/node/extensionManagementService.ts @@ -3,35 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { tmpdir } from 'os'; +import { generateUuid } from 'vs/base/common/uuid'; import { ILocalExtension, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ExtensionManagementService as BaseExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { Schemas } from 'vs/base/common/network'; +import * as path from 'vs/base/common/path'; export class ExtensionManagementService extends BaseExtensionManagementService { - async install(vsix: URI): Promise { - if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - const manifest = await getManifest(vsix.fsPath); - if (isLanguagePackExtension(manifest)) { - // Install on both servers - const [local] = await Promise.all(this.servers.map(server => server.extensionManagementService.install(vsix))); - return local; - } - if (isUIExtension(manifest, this.productService, this.configurationService)) { - // Install only on local server - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix); - } - // Install only on remote server - return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.install(vsix); + protected async installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { + if (vsix.scheme === Schemas.vscodeRemote && server === this.extensionManagementServerService.localExtensionManagementServer) { + const downloadedLocation = URI.file(path.join(tmpdir(), generateUuid())); + await this.downloadService.download(vsix, downloadedLocation); + vsix = downloadedLocation; } - if (this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix); - } - return Promise.reject('No Servers to Install'); + return server.extensionManagementService.install(vsix); } } diff --git a/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts index 5852ce2d53ce7..4536a5ad507ab 100644 --- a/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts @@ -16,12 +16,12 @@ import { IExtensionContributions, ExtensionType, IExtension } from 'vs/platform/ import { isUndefinedOrNull } from 'vs/base/common/types'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ProductService } from 'vs/platform/product/node/productService'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { assign } from 'vs/base/common/objects'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { productService } from 'vs/workbench/test/workbenchTestServices'; function storageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -46,7 +46,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidInstallExtension: new Emitter().event, onDidUninstallExtension: new Emitter().event } as IExtensionManagementService), instantiationService.get(IConfigurationService), instantiationService.get(IExtensionManagementServerService), - new ProductService() + productService ); } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 077598d16d68f..0f09ebb5b6277 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -18,10 +18,21 @@ import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/co import { RemoteExtensionHostClient, IInitDataProvider } from 'vs/workbench/services/extensions/common/remoteExtensionHostClient'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { WebWorkerExtensionHostStarter } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter'; +import { URI } from 'vs/base/common/uri'; +import { isWebExtension } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions'; +import { DeltaExtensionsResult } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { - private _remoteExtensionsEnvironmentData: IRemoteAgentEnvironment | null; + private _disposables = new DisposableStore(); + private _remoteExtensionsEnvironmentData: IRemoteAgentEnvironment | null = null; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -32,6 +43,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IFileService fileService: IFileService, @IProductService productService: IProductService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @IConfigurationService private readonly _configService: IConfigurationService, + @IStaticExtensionsService private readonly _staticExtensions: IStaticExtensionsService, ) { super( instantiationService, @@ -43,8 +56,19 @@ export class ExtensionService extends AbstractExtensionService implements IExten productService, ); - this._remoteExtensionsEnvironmentData = null; this._initialize(); + this._initFetchFileSystem(); + } + + dispose(): void { + this._disposables.dispose(); + super.dispose(); + } + + private _initFetchFileSystem(): void { + const provider = new FetchFileSystemProvider(); + this._disposables.add(this._fileService.registerProvider(Schemas.http, provider)); + this._disposables.add(this._fileService.registerProvider(Schemas.https, provider)); } private _createProvider(remoteAuthority: string): IInitDataProvider { @@ -58,36 +82,60 @@ export class ExtensionService extends AbstractExtensionService implements IExten }; } - protected _createExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] { + protected _createExtensionHosts(_isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] { const result: ExtensionHostProcessManager[] = []; - const remoteAgentConnection = this._remoteAgentService.getConnection()!; - const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); - const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents); - result.push(remoteExtHostProcessManager); + const webExtensions = this.getExtensions().then(extensions => extensions.filter(ext => isWebExtension(ext, this._configService))); + const webHostProcessWorker = this._instantiationService.createInstance(WebWorkerExtensionHostStarter, true, webExtensions, URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme })); + const webHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, webHostProcessWorker, null, initialActivationEvents); + result.push(webHostProcessManager); + + const remoteAgentConnection = this._remoteAgentService.getConnection(); + if (remoteAgentConnection) { + const remoteExtensions = this.getExtensions().then(extensions => extensions.filter(ext => !isWebExtension(ext, this._configService))); + const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, remoteExtensions, this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents); + result.push(remoteExtHostProcessManager); + } return result; } protected async _scanAndHandleExtensions(): Promise { // fetch the remote environment - const remoteEnv = (await this._remoteAgentService.getEnvironment())!; + let [remoteEnv, localExtensions] = await Promise.all([ + this._remoteAgentService.getEnvironment(), + this._staticExtensions.getExtensions() + ]); + + let result: DeltaExtensionsResult; - // enable or disable proposed API per extension - this._checkEnableProposedApi(remoteEnv.extensions); + // local: only enabled and web'ish extension + localExtensions = localExtensions.filter(ext => this._isEnabled(ext) && isWebExtension(ext, this._configService)); + this._checkEnableProposedApi(localExtensions); - // remove disabled extensions - remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension)); + if (!remoteEnv) { + result = this._registry.deltaExtensions(localExtensions, []); - // save for remote extension's init data - this._remoteExtensionsEnvironmentData = remoteEnv; + } else { + // remote: only enabled and none-web'ish extension + remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !isWebExtension(extension, this._configService)); + this._checkEnableProposedApi(remoteEnv.extensions); + + // in case of overlap, the remote wins + const isRemoteExtension = new Set(); + remoteEnv.extensions.forEach(extension => isRemoteExtension.add(ExtensionIdentifier.toKey(extension.identifier))); + localExtensions = localExtensions.filter(extension => !isRemoteExtension.has(ExtensionIdentifier.toKey(extension.identifier))); + + // save for remote extension's init data + this._remoteExtensionsEnvironmentData = remoteEnv; + + result = this._registry.deltaExtensions(remoteEnv.extensions.concat(localExtensions), []); + } - // this._handleExtensionPoints(([]).concat(remoteEnv.extensions).concat(localExtensions)); - const result = this._registry.deltaExtensions(remoteEnv.extensions, []); if (result.removedDueToLooping.length > 0) { this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); } - this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions()); } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts new file mode 100644 index 0000000000000..ef8569f52525a --- /dev/null +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions'; +import { IProductService } from 'vs/platform/product/common/product'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export class WebWorkerExtensionHostStarter implements IExtensionHostStarter { + + private _toDispose = new DisposableStore(); + private _isTerminating: boolean = false; + private _protocol?: IMessagePassingProtocol; + + private readonly _onDidExit = new Emitter<[number, string | null]>(); + readonly onExit: Event<[number, string | null]> = this._onDidExit.event; + + constructor( + private readonly _autoStart: boolean, + private readonly _extensions: Promise, + private readonly _extensionHostLogsLocation: URI, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @ILabelService private readonly _labelService: ILabelService, + @ILogService private readonly _logService: ILogService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IProductService private readonly _productService: IProductService, + ) { + + } + + async start(): Promise { + + if (!this._protocol) { + + const emitter = new Emitter(); + + const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost'); + const worker = new Worker(url); + + worker.onmessage = (event) => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + this._onDidExit.fire([77, 'UNKNOWN data received']); + return; + } + + emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); + }; + + worker.onerror = (event) => { + console.error(event.message, event.error); + this._onDidExit.fire([81, event.message || event.error]); + }; + + // keep for cleanup + this._toDispose.add(emitter); + this._toDispose.add(toDisposable(() => worker.terminate())); + + const protocol: IMessagePassingProtocol = { + onMessage: emitter.event, + send: vsbuf => { + const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); + worker.postMessage(data, [data]); + } + }; + + // extension host handshake happens below + // (1) <== wait for: Ready + // (2) ==> send: init data + // (3) <== wait for: Initialized + + await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready))); + protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData()))); + await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized))); + + this._protocol = protocol; + } + return this._protocol; + + } + + dispose(): void { + if (!this._protocol) { + this._toDispose.dispose(); + return; + } + if (this._isTerminating) { + return; + } + this._isTerminating = true; + this._protocol.send(createMessageOfType(MessageType.Terminate)); + setTimeout(() => this._toDispose.dispose(), 10 * 1000); + } + + getInspectPort(): number | undefined { + return undefined; + } + + private async _createExtHostInitData(): Promise { + const [telemetryInfo, extensionDescriptions] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions]); + const workspace = this._contextService.getWorkspace(); + return { + commit: this._productService.commit, + version: this._productService.version, + parentPid: -1, + environment: { + isExtensionDevelopmentDebug: false, + appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, + appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined, + appName: this._productService.nameLong, + appUriScheme: this._productService.urlProtocol, + appLanguage: platform.language, + extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, + extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, + globalStorageHome: URI.parse('fake:globalStorageHome'), //todo@joh URI.file(this._environmentService.globalStorageHome), + userHome: URI.parse('fake:userHome'), //todo@joh URI.file(this._environmentService.userHome), + webviewResourceRoot: this._environmentService.webviewResourceRoot, + webviewCspSource: this._environmentService.webviewCspSource, + }, + workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { + configuration: workspace.configuration || undefined, + id: workspace.id, + name: this._labelService.getWorkspaceLabel(workspace) + }, + resolvedExtensions: [], + hostExtensions: [], + extensions: extensionDescriptions, + telemetryInfo, + logLevel: this._logService.getLevel(), + logsLocation: this._extensionHostLogsLocation, + autoStart: this._autoStart, + remote: { + authority: this._environmentService.configuration.remoteAuthority, + isRemote: false + }, + }; + } +} diff --git a/src/vs/workbench/services/extensions/browser/webWorkerFileSystemProvider.ts b/src/vs/workbench/services/extensions/browser/webWorkerFileSystemProvider.ts new file mode 100644 index 0000000000000..13afbbcce1fad --- /dev/null +++ b/src/vs/workbench/services/extensions/browser/webWorkerFileSystemProvider.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IFileSystemProvider, FileSystemProviderCapabilities, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; + +import { Event } from 'vs/base/common/event'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { NotImplementedError } from 'vs/base/common/errors'; + +export class FetchFileSystemProvider implements IFileSystemProvider { + + readonly capabilities = FileSystemProviderCapabilities.Readonly + FileSystemProviderCapabilities.FileReadWrite + FileSystemProviderCapabilities.PathCaseSensitive; + readonly onDidChangeCapabilities = Event.None; + readonly onDidChangeFile = Event.None; + + // working implementations + async readFile(resource: URI): Promise { + try { + const res = await fetch(resource.toString(true)); + if (res.status === 200) { + return new Uint8Array(await res.arrayBuffer()); + } + throw new FileSystemProviderError(res.statusText, FileSystemProviderErrorCode.Unknown); + } catch (err) { + throw new FileSystemProviderError(err, FileSystemProviderErrorCode.Unknown); + } + } + + // fake implementations + async stat(_resource: URI): Promise { + return { + type: FileType.File, + size: 0, + mtime: 0, + ctime: 0 + }; + } + + watch(): IDisposable { + return Disposable.None; + } + + // error implementations + writeFile(_resource: URI, _content: Uint8Array, _opts: FileWriteOptions): Promise { + throw new NotImplementedError(); + } + readdir(_resource: URI): Promise<[string, FileType][]> { + throw new NotImplementedError(); + } + mkdir(_resource: URI): Promise { + throw new NotImplementedError(); + } + delete(_resource: URI, _opts: FileDeleteOptions): Promise { + throw new NotImplementedError(); + } + rename(_from: URI, _to: URI, _opts: FileOverwriteOptions): Promise { + throw new NotImplementedError(); + } +} diff --git a/src/vs/workbench/services/extensions/common/extensionHostMain.ts b/src/vs/workbench/services/extensions/common/extensionHostMain.ts index d1c8e25185bab..16356a4a96cba 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostMain.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostMain.ts @@ -10,7 +10,6 @@ import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; @@ -35,10 +34,6 @@ export interface IConsolePatchFn { (mainThreadConsole: MainThreadConsoleShape): any; } -export interface ILogServiceFn { - (initData: IInitData): ILogService; -} - export class ExtensionHostMain { private _isTerminating: boolean; @@ -50,8 +45,6 @@ export class ExtensionHostMain { protocol: IMessagePassingProtocol, initData: IInitData, hostUtils: IHostUtils, - consolePatchFn: IConsolePatchFn, - logServiceFn: ILogServiceFn, uriTransformer: IURITransformer | null ) { this._isTerminating = false; @@ -61,27 +54,27 @@ export class ExtensionHostMain { // ensure URIs are transformed and revived initData = ExtensionHostMain._transform(initData, rpcProtocol); - // allow to patch console - consolePatchFn(rpcProtocol.getProxy(MainContext.MainThreadConsole)); - - // services - const extHostLogService = new ExtHostLogService(logServiceFn(initData), initData.logsLocation.fsPath); - this._disposables.add(extHostLogService); - // bootstrap services const services = new ServiceCollection(...getSingletonServiceDescriptors()); services.set(IExtHostInitDataService, { _serviceBrand: undefined, ...initData }); services.set(IExtHostRpcService, new ExtHostRpcService(rpcProtocol)); - services.set(ILogService, extHostLogService); services.set(IURITransformerService, new URITransformerService(uriTransformer)); services.set(IHostUtils, hostUtils); const instaService: IInstantiationService = new InstantiationService(services, true); - extHostLogService.info('extension host started'); - extHostLogService.trace('initData', initData); + // todo@joh + // ugly self - inject + const logService = instaService.invokeFunction(accessor => accessor.get(ILogService)); + this._disposables.add(logService); + + logService.info('extension host started'); + logService.trace('initData', initData); - // todo@joh -> not soo nice... + // todo@joh + // ugly self - inject + // must call initialize *after* creating the extension service + // because `initialize` itself creates instances that depend on it this._extensionService = instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService)); this._extensionService.initialize(); diff --git a/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts index f8067e33e0d1c..44f76d95fd03e 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostCustomersRegistry } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; @@ -59,7 +59,7 @@ export class ExtensionHostProcessManager extends Disposable { private readonly _remoteAuthority: string, initialActivationEvents: string[], @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, ) { super(); this._extensionHostProcessFinishedActivateEvents = Object.create(null); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 9776a42da6ab4..f5fd3137e472e 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -61,9 +61,7 @@ export interface IExtensionPointUser { collector: ExtensionMessageCollector; } -export interface IExtensionPointHandler { - (extensions: IExtensionPointUser[], delta: ExtensionPointUserDelta): void; -} +export type IExtensionPointHandler = (extensions: readonly IExtensionPointUser[], delta: ExtensionPointUserDelta) => void; export interface IExtensionPoint { name: string; @@ -73,7 +71,7 @@ export interface IExtensionPoint { export class ExtensionPointUserDelta { - private static _toSet(arr: IExtensionPointUser[]): Set { + private static _toSet(arr: readonly IExtensionPointUser[]): Set { const result = new Set(); for (let i = 0, len = arr.length; i < len; i++) { result.add(ExtensionIdentifier.toKey(arr[i].description.identifier)); @@ -81,7 +79,7 @@ export class ExtensionPointUserDelta { return result; } - public static compute(previous: IExtensionPointUser[] | null, current: IExtensionPointUser[]): ExtensionPointUserDelta { + public static compute(previous: readonly IExtensionPointUser[] | null, current: readonly IExtensionPointUser[]): ExtensionPointUserDelta { if (!previous || !previous.length) { return new ExtensionPointUserDelta(current, []); } @@ -99,8 +97,8 @@ export class ExtensionPointUserDelta { } constructor( - public readonly added: IExtensionPointUser[], - public readonly removed: IExtensionPointUser[], + public readonly added: readonly IExtensionPointUser[], + public readonly removed: readonly IExtensionPointUser[], ) { } } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index e03ddb10239b0..a1496708db6d1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -10,6 +10,11 @@ import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionM import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IProductService } from 'vs/platform/product/common/product'; +export function isWebExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean { + const extensionKind = getExtensionKind(manifest, configurationService); + return extensionKind === 'web'; +} + export function isUIExtension(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean { const uiContributions = ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').map(e => e.name); const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts index 47aaf4a22bb3b..bcb46463c2c8b 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { connectRemoteAgentExtensionHost, IRemoteExtensionHostStartParams, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -49,7 +49,7 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH private readonly _initDataProvider: IInitDataProvider, private readonly _socketFactory: ISocketFactory, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @ILogService private readonly _logService: ILogService, @@ -79,7 +79,8 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH return { host: authority.host, port: authority.port }; } }, - signService: this._signService + signService: this._signService, + logService: this._logService }; return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolverResult) => { diff --git a/src/vs/workbench/services/extensions/common/staticExtensions.ts b/src/vs/workbench/services/extensions/common/staticExtensions.ts new file mode 100644 index 0000000000000..1d5e3f57b05c3 --- /dev/null +++ b/src/vs/workbench/services/extensions/common/staticExtensions.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export const IStaticExtensionsService = createDecorator('IStaticExtensionsService'); + +export interface IStaticExtensionsService { + _serviceBrand: any; + getExtensions(): Promise; +} + +export class StaticExtensionsService implements IStaticExtensionsService { + + _serviceBrand: any; + + private readonly _descriptions: IExtensionDescription[] = []; + + constructor(@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService) { + const staticExtensions = environmentService.options && Array.isArray(environmentService.options.staticExtensions) ? environmentService.options.staticExtensions : []; + + this._descriptions = staticExtensions.map(data => { + identifier: new ExtensionIdentifier(`${data.packageJSON.publisher}.${data.packageJSON.name}`), + extensionLocation: URI.revive(data.extensionLocation), + ...data.packageJSON, + }); + } + + async getExtensions(): Promise { + return this._descriptions; + } +} + +registerSingleton(IStaticExtensionsService, StaticExtensionsService, true); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 6fb7d2d1be1bd..b8c267ab59041 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -34,6 +34,8 @@ import { IFileService } from 'vs/platform/files/common/files'; import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; import { IProductService } from 'vs/platform/product/common/product'; import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { flatten } from 'vs/base/common/arrays'; +import { IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions'; class DeltaExtensionsQueueItem { constructor( @@ -64,6 +66,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IConfigurationService private readonly _configurationService: IConfigurationService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IWindowService protected readonly _windowService: IWindowService, + @IStaticExtensionsService private readonly _staticExtensions: IStaticExtensionsService, ) { super( instantiationService, @@ -72,7 +75,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten telemetryService, extensionEnablementService, fileService, - productService, + productService ); if (this._extensionEnablementService.allUserExtensionsDisabled) { @@ -349,6 +352,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } const result: ExtensionHostProcessManager[] = []; + const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._extensionHostLogsLocation); const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, true, extHostProcessWorker, null, initialActivationEvents); result.push(extHostProcessManager); @@ -431,7 +435,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const remoteAuthority = this._environmentService.configuration.remoteAuthority; const extensionHost = this._extensionHostProcessManagers[0]; - let localExtensions = await this._extensionScanner.scannedExtensions; + let localExtensions = flatten(await Promise.all([this._extensionScanner.scannedExtensions, this._staticExtensions.getExtensions()])); // enable or disable proposed API per extension this._checkEnableProposedApi(localExtensions); @@ -457,7 +461,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAuthorityResolverService.setResolvedAuthorityError(remoteAuthority, err); // Proceed with the local extension host - await this._startLocalExtensionHost(extensionHost, localExtensions); + await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier)); return; } @@ -502,20 +506,18 @@ export class ExtensionService extends AbstractExtensionService implements IExten // save for remote extension's init data this._remoteExtensionsEnvironmentData.set(remoteAuthority, remoteEnv); - this._handleExtensionPoints(([]).concat(remoteEnv.extensions).concat(localExtensions)); - extensionHost.start(localExtensions.map(extension => extension.identifier)); - + await this._startLocalExtensionHost(extensionHost, remoteEnv.extensions.concat(localExtensions), localExtensions.map(extension => extension.identifier)); } else { - await this._startLocalExtensionHost(extensionHost, localExtensions); + await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier)); } } - private async _startLocalExtensionHost(extensionHost: ExtensionHostProcessManager, localExtensions: IExtensionDescription[]): Promise { - this._handleExtensionPoints(localExtensions); - extensionHost.start(localExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + private async _startLocalExtensionHost(extensionHost: ExtensionHostProcessManager, allExtensions: IExtensionDescription[], localExtensions: ExtensionIdentifier[]): Promise { + this._registerAndHandleExtensions(allExtensions); + extensionHost.start(localExtensions.filter(id => this._registry.containsExtension(id))); } - private _handleExtensionPoints(allExtensions: IExtensionDescription[]): void { + private _registerAndHandleExtensions(allExtensions: IExtensionDescription[]): void { const result = this._registry.deltaExtensions(allExtensions, []); if (result.removedDueToLooping.length > 0) { this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); diff --git a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts b/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts index 84ec145c4c845..f94d89adb16b2 100644 --- a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts +++ b/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts @@ -140,4 +140,4 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC } return this.getDependenciesAndPackedExtensionsRecursively(toGet, result, uiExtension, token); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index f708e29da7c02..6d31b177acaa7 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -7,21 +7,19 @@ import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; import * as minimist from 'vscode-minimist'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol, ProtocolConstants, createBufferedEvent } from 'vs/base/parts/ipc/common/ipc.net'; +import { PersistentProtocol, ProtocolConstants, BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import product from 'vs/platform/product/node/product'; -import { IInitData, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostMain, IExitFn, ILogServiceFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; +import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; -import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; import { exists } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; -import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import 'vs/workbench/api/node/extHost.services'; interface ParsedExtHostArgs { @@ -64,27 +62,13 @@ function patchProcess(allowExit: boolean) { } } as (code?: number) => never; + // override Electron's process.crash() method process.crash = function () { const err = new Error('An extension called process.crash() and this was prevented.'); console.warn(err.stack); }; } -// use IPC messages to forward console-calls -function patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void { - // The console is already patched to use `process.send()` - const nativeProcessSend = process.send!; - process.send = (...args: any[]) => { - if (args.length === 0 || !args[0] || args[0].type !== '__$console') { - return nativeProcessSend.apply(process, args); - } - - mainThreadConsole.$logExtensionHostMessage(args[0]); - }; -} - -const createLogService: ILogServiceFn = initData => new SpdLogService(ExtensionHostLogFileName, initData.logsLocation.fsPath, initData.logLevel); - interface IRendererConnection { protocol: IMessagePassingProtocol; initData: IInitData; @@ -180,8 +164,8 @@ async function createExtHostProtocol(): Promise { return new class implements IMessagePassingProtocol { - private readonly _onMessage = new Emitter(); - readonly onMessage: Event = createBufferedEvent(this._onMessage.event); + private readonly _onMessage = new BufferedEmitter(); + readonly onMessage: Event = this._onMessage.event; private _terminating: boolean; @@ -206,7 +190,7 @@ async function createExtHostProtocol(): Promise { } function connectToRenderer(protocol: IMessagePassingProtocol): Promise { - return new Promise((c, e) => { + return new Promise((c) => { // Listen init data message const first = protocol.onMessage(raw => { @@ -335,8 +319,6 @@ export async function startExtensionHostProcess(): Promise { renderer.protocol, initData, hostUtils, - patchPatchedConsole, - createLogService, uriTransformer ); diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index f20f824a2e2c9..8abe6aa7f52c9 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -309,11 +309,6 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`; extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id); - // main := absolutePath(`main`) - if (extensionDescription.main) { - extensionDescription.main = path.join(this._absoluteFolderPath, extensionDescription.main); - } - extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath); return extensionDescription; diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 6aa6ba1e61797..12a6e3ea66a37 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -17,11 +17,11 @@ import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorksp import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { ProxyAgent } from 'vscode-proxy-agent'; import { MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; import { promisify } from 'util'; +import { ILogService } from 'vs/platform/log/common/log'; interface ConnectionResult { proxy: string; @@ -34,7 +34,7 @@ export function connectProxyResolver( extHostWorkspace: IExtHostWorkspaceProvider, configProvider: ExtHostConfigProvider, extensionService: ExtHostExtensionService, - extHostLogService: ExtHostLogService, + extHostLogService: ILogService, mainThreadTelemetry: MainThreadTelemetryShape ) { const resolveProxy = setupProxyResolution(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry); @@ -47,7 +47,7 @@ const maxCacheEntries = 5000; // Cache can grow twice that much due to 'oldCache function setupProxyResolution( extHostWorkspace: IExtHostWorkspaceProvider, configProvider: ExtHostConfigProvider, - extHostLogService: ExtHostLogService, + extHostLogService: ILogService, mainThreadTelemetry: MainThreadTelemetryShape ) { const env = process.env; @@ -421,7 +421,7 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku }); } -function useSystemCertificates(extHostLogService: ExtHostLogService, useSystemCertificates: boolean, opts: http.RequestOptions, callback: () => void) { +function useSystemCertificates(extHostLogService: ILogService, useSystemCertificates: boolean, opts: http.RequestOptions, callback: () => void) { if (useSystemCertificates) { getCaCertificates(extHostLogService) .then(caCertificates => { @@ -443,7 +443,7 @@ function useSystemCertificates(extHostLogService: ExtHostLogService, useSystemCe } let _caCertificates: ReturnType | Promise; -async function getCaCertificates(extHostLogService: ExtHostLogService) { +async function getCaCertificates(extHostLogService: ILogService) { if (!_caCertificates) { _caCertificates = readCaCertificates() .then(res => res && res.certs.length ? res : undefined) @@ -469,24 +469,26 @@ async function readCaCertificates() { } async function readWindowsCaCertificates() { - const winCA = await import('vscode-windows-ca-certs'); - - let ders: any[] = []; - const store = winCA(); - try { - let der: any; - while (der = store.next()) { - ders.push(der); - } - } finally { - store.done(); - } + // Not using await to work around minifier bug (https://github.com/microsoft/vscode/issues/79044). + return import('vscode-windows-ca-certs') + .then(winCA => { + let ders: any[] = []; + const store = winCA(); + try { + let der: any; + while (der = store.next()) { + ders.push(der); + } + } finally { + store.done(); + } - const certs = new Set(ders.map(derToPem)); - return { - certs: Array.from(certs), - append: true - }; + const certs = new Set(ders.map(derToPem)); + return { + certs: Array.from(certs), + append: true + }; + }); } async function readMacCaCertificates() { diff --git a/src/vs/workbench/services/extensions/worker/extHost.services.ts b/src/vs/workbench/services/extensions/worker/extHost.services.ts new file mode 100644 index 0000000000000..bf4a779155408 --- /dev/null +++ b/src/vs/workbench/services/extensions/worker/extHost.services.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtHostOutputService, ExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; +import { IExtHostWorkspace, ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { IExtHostDecorations, ExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations'; +import { IExtHostConfiguration, ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; +import { IExtHostCommands, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; +import { IExtHostTask } from 'vs/workbench/api/common/extHostTask'; +import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; +import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; +import { ExtHostExtensionService } from 'vs/workbench/api/worker/extHostExtensionService'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService'; + +// register singleton services +registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostOutputService, ExtHostOutputService); +registerSingleton(IExtHostWorkspace, ExtHostWorkspace); +registerSingleton(IExtHostDecorations, ExtHostDecorations); +registerSingleton(IExtHostConfiguration, ExtHostConfiguration); +registerSingleton(IExtHostCommands, ExtHostCommands); +registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors); +registerSingleton(IExtHostStorage, ExtHostStorage); +registerSingleton(IExtHostExtensionService, ExtHostExtensionService); + +// register services that only throw errors +function NotImplementedProxy(name: ServiceIdentifier): { new(): T } { + return class { + constructor() { + return new Proxy({}, { + get(target: any, prop: string | number) { + if (target[prop]) { + return target[prop]; + } + throw new Error(`Not Implemented: ${name}->${String(prop)}`); + } + }); + } + }; +} +registerSingleton(IExtHostTerminalService, class extends NotImplementedProxy(IExtHostTerminalService) { }); +registerSingleton(IExtHostTask, class extends NotImplementedProxy(IExtHostTask) { }); +registerSingleton(IExtHostDebugService, class extends NotImplementedProxy(IExtHostDebugService) { }); +registerSingleton(IExtHostSearch, class extends NotImplementedProxy(IExtHostSearch) { }); +registerSingleton(IExtensionStoragePaths, class extends NotImplementedProxy(IExtensionStoragePaths) { + whenReady = Promise.resolve(); +}); diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts new file mode 100644 index 0000000000000..3d6659698bed3 --- /dev/null +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; +import { isMessageOfType, MessageType, createMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtensionHostMain } from 'vs/workbench/services/extensions/common/extensionHostMain'; +import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; +import 'vs/workbench/services/extensions/worker/extHost.services'; + +//#region --- Define, capture, and override some globals +//todo@joh do not allow extensions to call postMessage and other globals... + +declare namespace self { + let close: any; + let postMessage: any; + let addEventLister: any; + let indexedDB: any; + let caches: any; +} + +const nativeClose = self.close.bind(self); +self.close = () => console.trace(`'close' has been blocked`); + +const nativePostMessage = postMessage.bind(self); +self.postMessage = () => console.trace(`'postMessage' has been blocked`); + +const nativeAddEventLister = addEventListener.bind(self); +self.addEventLister = () => console.trace(`'addEventListener' has been blocked`); + +// readonly, cannot redefine... +// self.indexedDB = undefined; +// self.caches = undefined; + +//#endregion --- + +const hostUtil = new class implements IHostUtils { + _serviceBrand: any; + exit(_code?: number | undefined): void { + nativeClose(); + } + async exists(_path: string): Promise { + return true; + } + async realpath(path: string): Promise { + return path; + } +}; + + +class ExtensionWorker { + + // protocol + readonly protocol: IMessagePassingProtocol; + + constructor() { + + let emitter = new Emitter(); + let terminating = false; + + + nativeAddEventLister('message', event => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + return; + } + + const msg = VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)); + if (isMessageOfType(msg, MessageType.Terminate)) { + // handle terminate-message right here + terminating = true; + onTerminate(); + return; + } + + // emit non-terminate messages to the outside + emitter.fire(msg); + }); + + this.protocol = { + onMessage: emitter.event, + send: vsbuf => { + if (!terminating) { + const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); + nativePostMessage(data, [data]); + } + } + }; + } +} + +interface IRendererConnection { + protocol: IMessagePassingProtocol; + initData: IInitData; +} +function connectToRenderer(protocol: IMessagePassingProtocol): Promise { + return new Promise(resolve => { + const once = protocol.onMessage(raw => { + once.dispose(); + const initData = JSON.parse(raw.toString()); + protocol.send(createMessageOfType(MessageType.Initialized)); + resolve({ protocol, initData }); + }); + protocol.send(createMessageOfType(MessageType.Ready)); + }); +} + +let onTerminate = nativeClose; + +(function create(): void { + const res = new ExtensionWorker(); + + connectToRenderer(res.protocol).then(data => { + + const extHostMain = new ExtensionHostMain( + data.protocol, + data.initData, + hostUtil, + null, + ); + + onTerminate = () => extHostMain.terminate(); + }); +})(); diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts new file mode 100644 index 0000000000000..79455414c06b9 --- /dev/null +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +(function () { + + let MonacoEnvironment = (self).MonacoEnvironment; + let monacoBaseUrl = MonacoEnvironment && MonacoEnvironment.baseUrl ? MonacoEnvironment.baseUrl : '../../../../../'; + + if (typeof (self).define !== 'function' || !(self).define.amd) { + importScripts(monacoBaseUrl + 'vs/loader.js'); + } + + require.config({ + baseUrl: monacoBaseUrl, + catchError: true + }); + + require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err)); +})(); diff --git a/src/vs/workbench/services/files/common/workspaceWatcher.ts b/src/vs/workbench/services/files/common/workspaceWatcher.ts index 7aa7ef157f688..da8c120643af5 100644 --- a/src/vs/workbench/services/files/common/workspaceWatcher.ts +++ b/src/vs/workbench/services/files/common/workspaceWatcher.ts @@ -13,10 +13,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { ResourceMap } from 'vs/base/common/map'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; import { FileService } from 'vs/platform/files/common/fileService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class WorkspaceWatcher extends Disposable { @@ -27,7 +27,7 @@ export class WorkspaceWatcher extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @INotificationService private readonly notificationService: INotificationService, - @IStorageService private readonly storageService: IStorageService + @IOpenerService private readonly openerService: IOpenerService ) { super(); @@ -73,38 +73,34 @@ export class WorkspaceWatcher extends Disposable { onUnexpectedError(msg); // Detect if we run < .NET Framework 4.5 - if (msg.indexOf('System.MissingMethodException') >= 0 && !this.storageService.getBoolean('ignoreNetVersionError', StorageScope.WORKSPACE)) { + if (msg.indexOf('System.MissingMethodException') >= 0) { this.notificationService.prompt( Severity.Warning, localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."), [{ label: localize('installNet', "Download .NET Framework 4.5"), - run: () => window.open('https://go.microsoft.com/fwlink/?LinkId=786533') - }, - { - label: localize('neverShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this.storageService.store('ignoreNetVersionError', true, StorageScope.WORKSPACE) + run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=786533')) }], - { sticky: true } + { + sticky: true, + neverShowAgain: { id: 'ignoreNetVersionError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE } + } ); } // Detect if we run into ENOSPC issues - if (msg.indexOf('ENOSPC') >= 0 && !this.storageService.getBoolean('ignoreEnospcError', StorageScope.WORKSPACE)) { + if (msg.indexOf('ENOSPC') >= 0) { this.notificationService.prompt( Severity.Warning, localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."), [{ label: localize('learnMore', "Instructions"), - run: () => window.open('https://go.microsoft.com/fwlink/?linkid=867693') - }, - { - label: localize('neverShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this.storageService.store('ignoreEnospcError', true, StorageScope.WORKSPACE) + run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693')) }], - { sticky: true } + { + sticky: true, + neverShowAgain: { id: 'ignoreEnospcError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE } + } ); } } @@ -157,4 +153,4 @@ export class WorkspaceWatcher extends Disposable { } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored); \ No newline at end of file +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 9ae41e0797795..23ad652c71c0f 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -453,7 +453,7 @@ export class HistoryService extends Disposable implements IHistoryService { this.doNavigate(this.stack[this.index], !acrossEditors).finally(() => this.navigatingInStack = false); } - private doNavigate(location: IStackEntry, withSelection: boolean): Promise { + private doNavigate(location: IStackEntry, withSelection: boolean): Promise { const options: ITextEditorOptions = { revealIfOpened: true // support to navigate across editor groups }; diff --git a/src/vs/workbench/services/integrity/browser/integrityService.ts b/src/vs/workbench/services/integrity/browser/integrityService.ts new file mode 100644 index 0000000000000..5ddf9d5b3f961 --- /dev/null +++ b/src/vs/workbench/services/integrity/browser/integrityService.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IIntegrityService, IntegrityTestResult } from 'vs/workbench/services/integrity/common/integrity'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; + +export class BrowserIntegrityServiceImpl implements IIntegrityService { + + _serviceBrand!: ServiceIdentifier; + + async isPure(): Promise { + return { isPure: true, proof: [] }; + } +} + +registerSingleton(IIntegrityService, BrowserIntegrityServiceImpl, true); diff --git a/src/vs/workbench/services/integrity/node/integrityService.ts b/src/vs/workbench/services/integrity/node/integrityService.ts index 00e083a9b9910..36c80d8c1485f 100644 --- a/src/vs/workbench/services/integrity/node/integrityService.ts +++ b/src/vs/workbench/services/integrity/node/integrityService.ts @@ -14,6 +14,8 @@ import product from 'vs/platform/product/node/product'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; interface IStorageData { dontShowPrompt: boolean; @@ -55,7 +57,7 @@ class IntegrityStorage { export class IntegrityServiceImpl implements IIntegrityService { - _serviceBrand: any; + _serviceBrand!: ServiceIdentifier; private _storage: IntegrityStorage; private _isPurePromise: Promise; @@ -63,7 +65,8 @@ export class IntegrityServiceImpl implements IIntegrityService { constructor( @INotificationService private readonly notificationService: INotificationService, @IStorageService storageService: IStorageService, - @ILifecycleService private readonly lifecycleService: ILifecycleService + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IOpenerService private readonly openerService: IOpenerService ) { this._storage = new IntegrityStorage(storageService); @@ -90,7 +93,7 @@ export class IntegrityServiceImpl implements IIntegrityService { [ { label: nls.localize('integrity.moreInformation', "More Information"), - run: () => window.open(URI.parse(product.checksumFailMoreInfoUrl).toString(true)) + run: () => this.openerService.open(URI.parse(product.checksumFailMoreInfoUrl)) }, { label: nls.localize('integrity.dontShowAgain', "Don't Show Again"), @@ -159,4 +162,4 @@ export class IntegrityServiceImpl implements IIntegrityService { } } -registerSingleton(IIntegrityService, IntegrityServiceImpl, true); \ No newline at end of file +registerSingleton(IIntegrityService, IntegrityServiceImpl, true); diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index c9394b52fc833..8fe901128a923 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -34,7 +34,6 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -// tslint:disable-next-line: import-patterns import { commandsExtensionPoint } from 'vs/workbench/api/common/menusExtensionPoint'; import { Disposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -45,7 +44,7 @@ import * as objects from 'vs/base/common/objects'; import { IKeymapService } from 'vs/workbench/services/keybinding/common/keymapInfo'; import { getDispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig'; import { isArray } from 'vs/base/common/types'; -import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/common/navigatorKeyboard'; +import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/browser/navigatorKeyboard'; import { ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode'; interface ContributedKeyBinding { diff --git a/src/vs/workbench/services/keybinding/browser/keymapService.ts b/src/vs/workbench/services/keybinding/browser/keymapService.ts index faacc11fbf5e2..e3744643efd64 100644 --- a/src/vs/workbench/services/keybinding/browser/keymapService.ts +++ b/src/vs/workbench/services/keybinding/browser/keymapService.ts @@ -25,7 +25,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ConfigExtensions, IConfigurationRegistry, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/common/navigatorKeyboard'; +import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/browser/navigatorKeyboard'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -176,6 +176,7 @@ export class BrowserKeyboardMapperFactoryBase { } setActiveKeyMapping(keymap: IKeyboardMapping | null) { + let keymapUpdated = false; let matchedKeyboardLayout = this.getMatchedKeymapInfo(keymap); if (matchedKeyboardLayout) { // let score = matchedKeyboardLayout.score; @@ -209,18 +210,21 @@ export class BrowserKeyboardMapperFactoryBase { if (!this._activeKeymapInfo) { this._activeKeymapInfo = matchedKeyboardLayout.result; + keymapUpdated = true; } else if (keymap) { if (matchedKeyboardLayout.result.getScore(keymap) > this._activeKeymapInfo.getScore(keymap)) { this._activeKeymapInfo = matchedKeyboardLayout.result; + keymapUpdated = true; } } } if (!this._activeKeymapInfo) { this._activeKeymapInfo = this.getUSStandardLayout(); + keymapUpdated = true; } - if (!this._activeKeymapInfo) { + if (!this._activeKeymapInfo || !keymapUpdated) { return; } diff --git a/src/vs/workbench/services/keybinding/common/navigatorKeyboard.ts b/src/vs/workbench/services/keybinding/browser/navigatorKeyboard.ts similarity index 100% rename from src/vs/workbench/services/keybinding/common/navigatorKeyboard.ts rename to src/vs/workbench/services/keybinding/browser/navigatorKeyboard.ts diff --git a/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts index e6f8b9b4d9719..a01c00e3180e8 100644 --- a/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts @@ -40,12 +40,12 @@ suite('keyboard layout loader', () => { let commandService = instantiationService.stub(ICommandService, {}); let instance = new TestKeyboardMapperFactory(notitifcationService, storageService, commandService); - test.skip('load default US keyboard layout', () => { + test('load default US keyboard layout', () => { assert.notEqual(instance.activeKeyboardLayout, null); - assert.equal(instance.activeKeyboardLayout!.isUSStandard, true); }); - test.skip('isKeyMappingActive', () => { + test('isKeyMappingActive', () => { + instance.setUSKeyboardLayout(); assert.equal(instance.isKeyMappingActive({ KeyA: { value: 'a', diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index a8e2290be22a1..f383c481e295e 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -6,6 +6,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; +import * as paths from 'vs/base/common/path'; import { Event, Emitter } from 'vs/base/common/event'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -128,7 +129,10 @@ export class LabelService implements ILabelService { } getUriLabel(resource: URI, options: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean } = {}): string { - const formatting = this.findFormatting(resource); + return this.doGetUriLabel(resource, this.findFormatting(resource), options); + } + + private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean } = {}): string { if (!formatting) { return getPathLabel(resource.path, this.environmentService, options.relative ? this.contextService : undefined); } @@ -159,6 +163,19 @@ export class LabelService implements ILabelService { return options.endWithSeparator ? this.appendSeparatorIfMissing(label, formatting) : label; } + getUriBasenameLabel(resource: URI): string { + const formatting = this.findFormatting(resource); + const label = this.doGetUriLabel(resource, formatting); + if (formatting) { + switch (formatting.separator) { + case paths.win32.sep: return paths.win32.basename(label); + case paths.posix.sep: return paths.posix.basename(label); + } + } + + return paths.basename(label); + } + getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string { if (IWorkspace.isIWorkspace(workspace)) { const identifier = toWorkspaceIdentifier(workspace); diff --git a/src/vs/workbench/services/label/test/label.test.ts b/src/vs/workbench/services/label/test/label.test.ts index ee4650f4d2ba4..a0d8af36a201f 100644 --- a/src/vs/workbench/services/label/test/label.test.ts +++ b/src/vs/workbench/services/label/test/label.test.ts @@ -33,9 +33,11 @@ suite('URI Label', () => { const uri1 = TestWorkspace.folders[0].uri.with({ path: TestWorkspace.folders[0].uri.path.concat('/a/b/c/d') }); assert.equal(labelService.getUriLabel(uri1, { relative: true }), isWindows ? 'a\\b\\c\\d' : 'a/b/c/d'); assert.equal(labelService.getUriLabel(uri1, { relative: false }), isWindows ? 'C:\\testWorkspace\\a\\b\\c\\d' : '/testWorkspace/a/b/c/d'); + assert.equal(labelService.getUriBasenameLabel(uri1), 'd'); const uri2 = URI.file('c:\\1/2/3'); assert.equal(labelService.getUriLabel(uri2, { relative: false }), isWindows ? 'C:\\1\\2\\3' : '/c:\\1/2/3'); + assert.equal(labelService.getUriBasenameLabel(uri2), '3'); }); test('custom scheme', function () { @@ -51,6 +53,23 @@ suite('URI Label', () => { const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL//1/2/3/4/5/microsoft.com/END'); + assert.equal(labelService.getUriBasenameLabel(uri1), 'END'); + }); + + test('separator', function () { + labelService.registerFormatter({ + scheme: 'vscode', + formatting: { + label: 'LABEL\\${path}\\${authority}\\END', + separator: '\\', + tildify: true, + normalizeDriveLetter: true + } + }); + + const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); + assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL\\\\1\\2\\3\\4\\5\\microsoft.com\\END'); + assert.equal(labelService.getUriBasenameLabel(uri1), 'END'); }); test('custom authority', function () { @@ -65,6 +84,7 @@ suite('URI Label', () => { const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL//1/2/3/4/5/microsoft.com/END'); + assert.equal(labelService.getUriBasenameLabel(uri1), 'END'); }); test('mulitple authority', function () { @@ -96,6 +116,7 @@ suite('URI Label', () => { // Make sure the most specific authority is picked const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'second'); + assert.equal(labelService.getUriBasenameLabel(uri1), 'second'); }); test('custom query', function () { diff --git a/src/vs/workbench/services/log/browser/indexedDBLogProvider.ts b/src/vs/workbench/services/log/browser/indexedDBLogProvider.ts new file mode 100644 index 0000000000000..16264dd585165 --- /dev/null +++ b/src/vs/workbench/services/log/browser/indexedDBLogProvider.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyValueLogProvider } from 'vs/workbench/services/log/common/keyValueLogProvider'; + +export const INDEXEDDB_VSCODE_DB = 'vscode-web-db'; +export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store'; + +export class IndexedDBLogProvider extends KeyValueLogProvider { + + readonly database: Promise; + + constructor(scheme: string) { + super(scheme); + this.database = this.openDatabase(1); + } + + private openDatabase(version: number): Promise { + return new Promise((c, e) => { + const request = window.indexedDB.open(INDEXEDDB_VSCODE_DB, version); + request.onerror = (err) => e(request.error); + request.onsuccess = () => { + const db = request.result; + if (db.objectStoreNames.contains(INDEXEDDB_LOGS_OBJECT_STORE)) { + c(db); + } + }; + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(INDEXEDDB_LOGS_OBJECT_STORE)) { + db.createObjectStore(INDEXEDDB_LOGS_OBJECT_STORE); + } + c(db); + }; + }); + } + + protected async getAllKeys(): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.getAllKeys(); + request.onerror = () => e(request.error); + request.onsuccess = () => c(request.result); + }); + } + + protected hasKey(key: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.getKey(key); + request.onerror = () => e(request.error); + request.onsuccess = () => { + c(!!request.result); + }; + }); + } + + protected getValue(key: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.get(key); + request.onerror = () => e(request.error); + request.onsuccess = () => c(request.result || ''); + }); + } + + protected setValue(key: string, value: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE], 'readwrite'); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.put(value, key); + request.onerror = () => e(request.error); + request.onsuccess = () => c(); + }); + } + + protected deleteKey(key: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE], 'readwrite'); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.delete(key); + request.onerror = () => e(request.error); + request.onsuccess = () => c(); + }); + } +} diff --git a/src/vs/workbench/services/log/common/inMemoryLogProvider.ts b/src/vs/workbench/services/log/common/inMemoryLogProvider.ts new file mode 100644 index 0000000000000..f8d87167c6e8c --- /dev/null +++ b/src/vs/workbench/services/log/common/inMemoryLogProvider.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyValueLogProvider } from 'vs/workbench/services/log/common/keyValueLogProvider'; +import { keys } from 'vs/base/common/map'; + +export class InMemoryLogProvider extends KeyValueLogProvider { + + private readonly logs: Map = new Map(); + + protected async getAllKeys(): Promise { + return keys(this.logs); + } + + protected async hasKey(key: string): Promise { + return this.logs.has(key); + } + + protected async getValue(key: string): Promise { + return this.logs.get(key) || ''; + } + + protected async setValue(key: string, value: string): Promise { + this.logs.set(key, value); + } + + protected async deleteKey(key: string): Promise { + this.logs.delete(key); + } + +} diff --git a/src/vs/workbench/services/log/common/keyValueLogProvider.ts b/src/vs/workbench/services/log/common/keyValueLogProvider.ts new file mode 100644 index 0000000000000..8bcdb579c758b --- /dev/null +++ b/src/vs/workbench/services/log/common/keyValueLogProvider.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { FileSystemError } from 'vs/workbench/api/common/extHostTypes'; +import { isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resources'; +import { values } from 'vs/base/common/map'; + +export abstract class KeyValueLogProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { + + readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite; + readonly onDidChangeCapabilities: Event = Event.None; + + private readonly _onDidChangeFile: Emitter = this._register(new Emitter()); + readonly onDidChangeFile: Event = this._onDidChangeFile.event; + + private readonly versions: Map = new Map(); + + constructor(private readonly scheme: string) { + super(); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + return Disposable.None; + } + + async mkdir(resource: URI): Promise { + } + + async stat(resource: URI): Promise { + try { + const content = await this.readFile(resource); + return { + type: FileType.File, + ctime: 0, + mtime: this.versions.get(resource.toString()) || 0, + size: content.byteLength + }; + } catch (e) { + } + const files = await this.readdir(resource); + if (files.length) { + return { + type: FileType.Directory, + ctime: 0, + mtime: 0, + size: 0 + }; + } + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotFound)); + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + const hasKey = await this.hasKey(resource.path); + if (hasKey) { + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotADirectory)); + } + const keys = await this.getAllKeys(); + const files: Map = new Map(); + for (const key of keys) { + const keyResource = this.toResource(key); + if (isEqualOrParent(keyResource, resource, false)) { + const path = relativePath(resource, keyResource, false); + if (path) { + const keySegments = path.split('/'); + files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]); + } + } + } + return values(files); + } + + async readFile(resource: URI): Promise { + const hasKey = await this.hasKey(resource.path); + if (!hasKey) { + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotFound)); + } + const value = await this.getValue(resource.path); + return VSBuffer.fromString(value).buffer; + } + + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + const hasKey = await this.hasKey(resource.path); + if (!hasKey) { + const files = await this.readdir(resource); + if (files.length) { + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileIsADirectory)); + } + } + await this.setValue(resource.path, VSBuffer.wrap(content).toString()); + this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1); + this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]); + } + + async delete(resource: URI, opts: FileDeleteOptions): Promise { + const hasKey = await this.hasKey(resource.path); + if (hasKey) { + await this.deleteKey(resource.path); + this.versions.delete(resource.path); + this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]); + return; + } + + if (opts.recursive) { + const files = await this.readdir(resource); + await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts))); + } + } + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + return Promise.reject(new Error('Not Supported')); + } + + private toResource(key: string): URI { + return URI.file(key).with({ scheme: this.scheme }); + } + + protected abstract getAllKeys(): Promise; + protected abstract hasKey(key: string): Promise; + protected abstract getValue(key: string): Promise; + protected abstract setValue(key: string, value: string): Promise; + protected abstract deleteKey(key: string): Promise; +} diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 81ba282a7c359..e701a079c296c 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import { INotificationsModel, NotificationsModel, ChoiceAction } from 'vs/workbench/common/notifications'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; @@ -63,11 +63,12 @@ export class NotificationService extends Disposable implements INotificationServ // Handle neverShowAgain option accordingly let handle: INotificationHandle; if (notification.neverShowAgain) { + const scope = notification.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; // If the user already picked to not show the notification // again, we return with a no-op notification here const id = notification.neverShowAgain.id; - if (this.storageService.getBoolean(id, StorageScope.GLOBAL)) { + if (this.storageService.getBoolean(id, scope)) { return new NoOpNotification(); } @@ -80,7 +81,7 @@ export class NotificationService extends Disposable implements INotificationServ handle.close(); // Remember choice - this.storageService.store(id, true, StorageScope.GLOBAL); + this.storageService.store(id, true, scope); return Promise.resolve(); })); @@ -110,17 +111,18 @@ export class NotificationService extends Disposable implements INotificationServ // Handle neverShowAgain option accordingly if (options && options.neverShowAgain) { + const scope = options.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; // If the user already picked to not show the notification // again, we return with a no-op notification here const id = options.neverShowAgain.id; - if (this.storageService.getBoolean(id, StorageScope.GLOBAL)) { + if (this.storageService.getBoolean(id, scope)) { return new NoOpNotification(); } const neverShowAgainChoice = { label: nls.localize('neverShowAgain', "Don't Show Again"), - run: () => this.storageService.store(id, true, StorageScope.GLOBAL), + run: () => this.storageService.store(id, true, scope), isSecondary: options.neverShowAgain.isSecondary }; diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 8788a416741e3..c95a613854514 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -38,6 +38,7 @@ import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSetti import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { withNullAsUndefined } from 'vs/base/common/types'; const emptyEditableSettingsContent = '{\n}'; @@ -188,15 +189,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic return null; } - openRawDefaultSettings(): Promise { + openRawDefaultSettings(): Promise { return this.editorService.openEditor({ resource: this.defaultSettingsRawResource }); } - openRawUserSettings(): Promise { + openRawUserSettings(): Promise { return this.editorService.openEditor({ resource: this.userSettingsResource }); } - openSettings(jsonEditor: boolean | undefined, query: string | undefined): Promise { + openSettings(jsonEditor: boolean | undefined, query: string | undefined): Promise { jsonEditor = typeof jsonEditor === 'undefined' ? this.configurationService.getValue('workbench.settings.editor') === 'json' : jsonEditor; @@ -213,11 +214,11 @@ export class PreferencesService extends Disposable implements IPreferencesServic private openSettings2(options?: ISettingsEditorOptions): Promise { const input = this.settingsEditor2Input; - return this.editorGroupService.activeGroup.openEditor(input, options) + return this.editorService.openEditor(input, options) .then(() => this.editorGroupService.activeGroup.activeControl!); } - openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { + openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { jsonEditor = typeof jsonEditor === 'undefined' ? this.configurationService.getValue('workbench.settings.editor') === 'json' : jsonEditor; @@ -227,16 +228,16 @@ export class PreferencesService extends Disposable implements IPreferencesServic this.openOrSwitchSettings2(ConfigurationTarget.USER_LOCAL, undefined, options, group); } - async openRemoteSettings(): Promise { + async openRemoteSettings(): Promise { const environment = await this.remoteAgentService.getEnvironment(); if (environment) { await this.createIfNotExists(environment.settingsPath, emptyEditableSettingsContent); return this.editorService.openEditor({ resource: environment.settingsPath, options: { pinned: true, revealIfOpened: true } }); } - return null; + return undefined; } - openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { + openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { jsonEditor = typeof jsonEditor === 'undefined' ? this.configurationService.getValue('workbench.settings.editor') === 'json' : jsonEditor; @@ -251,7 +252,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic this.openOrSwitchSettings2(ConfigurationTarget.WORKSPACE, undefined, options, group); } - async openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { + async openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { jsonEditor = typeof jsonEditor === 'undefined' ? this.configurationService.getValue('workbench.settings.editor') === 'json' : jsonEditor; @@ -306,7 +307,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true, revealIfOpened: true }).then(() => undefined); } - openDefaultKeybindingsFile(): Promise { + openDefaultKeybindingsFile(): Promise { return this.editorService.openEditor({ resource: this.defaultKeybindingsResource, label: nls.localize('defaultKeybindings', "Default Keybindings") }); } @@ -328,7 +329,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic })); } - private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise { + private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise { const editorInput = this.getActiveSettingsEditorInput(group); if (editorInput) { const editorInputResource = editorInput.master.getResource(); @@ -339,11 +340,11 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.doOpenSettings(configurationTarget, resource, options, group); } - private openOrSwitchSettings2(configurationTarget: ConfigurationTarget, folderUri?: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise { + private openOrSwitchSettings2(configurationTarget: ConfigurationTarget, folderUri?: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise { return this.doOpenSettings2(configurationTarget, folderUri, options, group); } - private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { + private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { const openSplitJSON = !!this.configurationService.getValue(USE_SPLIT_JSON_SETTING); if (openSplitJSON) { return this.doOpenSplitJSON(configurationTarget, resource, options, group); @@ -365,14 +366,14 @@ export class PreferencesService extends Disposable implements IPreferencesServic return Promise.all([ this.editorService.openEditor({ resource: this.defaultSettingsRawResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true }, label: nls.localize('defaultSettings', "Default Settings"), description: '' }), this.editorService.openEditor(editableSettingsEditorInput, { pinned: true, revealIfOpened: true }, sideEditorGroup.id) - ]).then(([defaultEditor, editor]) => editor); + ]).then(([defaultEditor, editor]) => withNullAsUndefined(editor)); } else { return this.editorService.openEditor(editableSettingsEditorInput, SettingsEditorOptions.create(options), group); } }); } - private doOpenSplitJSON(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { + private doOpenSplitJSON(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource) .then(editableSettingsEditorInput => { if (!options) { @@ -392,7 +393,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL)); } - private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): Promise { + private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): Promise { const input = this.settingsEditor2Input; const settingsOptions: ISettingsEditorOptions = { ...options, @@ -633,4 +634,4 @@ export class PreferencesService extends Disposable implements IPreferencesServic } } -registerSingleton(IPreferencesService, PreferencesService); \ No newline at end of file +registerSingleton(IPreferencesService, PreferencesService); diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index abf288178cc3a..195631aa108d0 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -201,15 +201,15 @@ export interface IPreferencesService { createPreferencesEditorModel(uri: URI): Promise | null>; createSettings2EditorModel(): Settings2EditorModel; // TODO - openRawDefaultSettings(): Promise; - openSettings(jsonEditor: boolean | undefined, query: string | undefined): Promise; - openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; - openRemoteSettings(): Promise; - openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; - openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; + openRawDefaultSettings(): Promise; + openSettings(jsonEditor: boolean | undefined, query: string | undefined): Promise; + openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; + openRemoteSettings(): Promise; + openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; + openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Promise; openGlobalKeybindingSettings(textual: boolean): Promise; - openDefaultKeybindingsFile(): Promise; + openDefaultKeybindingsFile(): Promise; configureSettingsForLanguage(language: string | null): void; } diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index d24d2a53f6803..2f334c3ca5a51 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -23,6 +23,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorModel } from 'vs/workbench/common/editor'; import { IFilterMetadata, IFilterResult, IGroupFilter, IKeybindingsEditorModel, ISearchResultGroup, ISetting, ISettingMatch, ISettingMatcher, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; import { withNullAsUndefined, isArray } from 'vs/base/common/types'; +import { FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; export const nullRange: IRange = { startLineNumber: -1, startColumn: -1, endLineNumber: -1, endColumn: -1 }; export function isNullRange(range: IRange): boolean { return range.startLineNumber === -1 && range.startColumn === -1 && range.endLineNumber === -1 && range.endColumn === -1; } @@ -659,11 +660,14 @@ export class DefaultSettings extends Disposable { } private matchesScope(property: IConfigurationNode): boolean { + if (!property.scope) { + return true; + } if (this.target === ConfigurationTarget.WORKSPACE_FOLDER) { - return property.scope === ConfigurationScope.RESOURCE; + return FOLDER_SCOPES.indexOf(property.scope) !== -1; } if (this.target === ConfigurationTarget.WORKSPACE) { - return property.scope === ConfigurationScope.WINDOW || property.scope === ConfigurationScope.RESOURCE; + return WORKSPACE_SCOPES.indexOf(property.scope) !== -1; } return true; } @@ -1024,6 +1028,67 @@ class SettingsContentBuilder { } export function createValidator(prop: IConfigurationPropertySchema): (value: any) => (string | null) { + // Only for array of string + if (prop.type === 'array' && prop.items && !isArray(prop.items) && prop.items.type === 'string') { + const propItems = prop.items; + if (propItems && !isArray(propItems) && propItems.type === 'string') { + const withQuotes = (s: string) => `'` + s + `'`; + + return value => { + if (!value) { + return null; + } + + let message = ''; + + const stringArrayValue = value as string[]; + + if (prop.minItems && stringArrayValue.length < prop.minItems) { + message += nls.localize('validations.stringArrayMinItem', 'Array must have at least {0} items', prop.minItems); + message += '\n'; + } + + if (prop.maxItems && stringArrayValue.length > prop.maxItems) { + message += nls.localize('validations.stringArrayMaxItem', 'Array must have less than {0} items', prop.maxItems); + message += '\n'; + } + + if (typeof propItems.pattern === 'string') { + const patternRegex = new RegExp(propItems.pattern); + stringArrayValue.forEach(v => { + if (!patternRegex.test(v)) { + message += + propItems.patternErrorMessage || + nls.localize( + 'validations.stringArrayItemPattern', + 'Value {0} must match regex {1}.', + withQuotes(v), + withQuotes(propItems.pattern!) + ); + } + }); + } + + const propItemsEnum = propItems.enum; + if (propItemsEnum) { + stringArrayValue.forEach(v => { + if (propItemsEnum.indexOf(v) === -1) { + message += nls.localize( + 'validations.stringArrayItemEnum', + 'Value {0} is not one of {1}', + withQuotes(v), + '[' + propItemsEnum.map(withQuotes).join(', ') + ']' + ); + message += '\n'; + } + }); + } + + return message; + }; + } + } + return value => { let exclusiveMax: number | undefined; let exclusiveMin: number | undefined; diff --git a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts index 555da0949aa12..b02e29c0c9615 100644 --- a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts +++ b/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts @@ -250,4 +250,82 @@ suite('Preferences Model test', () => { withMessage.rejects(' ').withMessage('always error!'); withMessage.rejects('1').withMessage('always error!'); }); -}); \ No newline at end of file + + class ArrayTester { + private validator: (value: any) => string | null; + + constructor(private settings: IConfigurationPropertySchema) { + this.validator = createValidator(settings)!; + } + + public accepts(input: string[]) { + assert.equal(this.validator(input), '', `Expected ${JSON.stringify(this.settings)} to accept \`${JSON.stringify(input)}\`. Got ${this.validator(input)}.`); + } + + public rejects(input: any[]) { + assert.notEqual(this.validator(input), '', `Expected ${JSON.stringify(this.settings)} to reject \`${JSON.stringify(input)}\`.`); + return { + withMessage: + (message: string) => { + const actual = this.validator(input); + assert.ok(actual); + assert(actual!.indexOf(message) > -1, + `Expected error of ${JSON.stringify(this.settings)} on \`${input}\` to contain ${message}. Got ${this.validator(input)}.`); + } + }; + } + } + + test('simple array', () => { + { + const arr = new ArrayTester({ type: 'array', items: { type: 'string' } }); + arr.accepts([]); + arr.accepts(['foo']); + arr.accepts(['foo', 'bar']); + } + }); + + test('min-max items array', () => { + { + const arr = new ArrayTester({ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 }); + arr.rejects([]).withMessage('Array must have at least 1 items'); + arr.accepts(['a']); + arr.accepts(['a', 'a']); + arr.rejects(['a', 'a', 'a']).withMessage('Array must have less than 2 items'); + } + }); + + test('array of enums', () => { + { + const arr = new ArrayTester({ type: 'array', items: { type: 'string', enum: ['a', 'b'] } }); + arr.accepts(['a']); + arr.accepts(['a', 'b']); + + arr.rejects(['c']).withMessage(`Value 'c' is not one of`); + arr.rejects(['a', 'c']).withMessage(`Value 'c' is not one of`); + + arr.rejects(['c', 'd']).withMessage(`Value 'c' is not one of`); + arr.rejects(['c', 'd']).withMessage(`Value 'd' is not one of`); + } + }); + + test('min-max and enum', () => { + const arr = new ArrayTester({ type: 'array', items: { type: 'string', enum: ['a', 'b'] }, minItems: 1, maxItems: 2 }); + + arr.rejects(['a', 'b', 'c']).withMessage('Array must have less than 2 items'); + arr.rejects(['a', 'b', 'c']).withMessage(`Value 'c' is not one of`); + }); + + test('pattern', () => { + const arr = new ArrayTester({ type: 'array', items: { type: 'string', pattern: '^(hello)*$' } }); + + arr.accepts(['hello']); + arr.rejects(['a']).withMessage(`Value 'a' must match regex`); + }); + + test('pattern with error message', () => { + const arr = new ArrayTester({ type: 'array', items: { type: 'string', pattern: '^(hello)*$', patternErrorMessage: 'err: must be friendly' } }); + + arr.rejects(['a']).withMessage(`err: must be friendly`); + }); +}); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 465920cd05ded..d4cef4f51f4dd 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -46,7 +46,7 @@ export class ProgressService extends Disposable implements IProgressService { super(); } - withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise { + withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise { const { location } = options; if (typeof location === 'string') { if (this.viewletService.getProgressIndicator(location)) { @@ -142,7 +142,7 @@ export class ProgressService extends Disposable implements IProgressService { } } - private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P { + private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: (choice?: number) => void): P { const toDispose = new DisposableStore(); const createNotification = (message: string | undefined, increment?: number): INotificationHandle | undefined => { @@ -152,6 +152,29 @@ export class ProgressService extends Disposable implements IProgressService { const primaryActions = options.primaryActions ? Array.from(options.primaryActions) : []; const secondaryActions = options.secondaryActions ? Array.from(options.secondaryActions) : []; + + if (options.buttons) { + options.buttons.forEach((button, index) => { + const buttonAction = new class extends Action { + constructor() { + super(`progress.button.${button}`, button, undefined, true); + } + + run(): Promise { + if (typeof onDidCancel === 'function') { + onDidCancel(index); + } + + return Promise.resolve(undefined); + } + }; + + toDispose.add(buttonAction); + + primaryActions.push(buttonAction); + }); + } + if (options.cancellable) { const cancelAction = new class extends Action { constructor() { @@ -182,6 +205,10 @@ export class ProgressService extends Disposable implements IProgressService { updateProgress(handle, increment); Event.once(handle.onDidClose)(() => { + if (typeof onDidCancel === 'function') { + onDidCancel(); + } + toDispose.dispose(); }); @@ -317,7 +344,7 @@ export class ProgressService extends Disposable implements IProgressService { return promise; } - private withDialogProgress

, R = unknown>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: () => void): P { + private withDialogProgress

, R = unknown>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: (choice?: number) => void): P { const disposables = new DisposableStore(); const allowableCommands = [ 'workbench.action.quit', @@ -327,12 +354,17 @@ export class ProgressService extends Disposable implements IProgressService { let dialog: Dialog; const createDialog = (message: string) => { + + const buttons = options.buttons || []; + buttons.push(options.cancellable ? localize('cancel', "Cancel") : localize('dismiss', "Dismiss")); + dialog = new Dialog( this.layoutService.container, message, - [options.cancellable ? localize('cancel', "Cancel") : localize('dismiss', "Dismiss")], + buttons, { type: 'pending', + cancelId: buttons.length - 1, keyEventProcessor: (event: StandardKeyboardEvent) => { const resolved = this.keybindingService.softDispatch(event, this.layoutService.container); if (resolved && resolved.commandId) { @@ -347,9 +379,9 @@ export class ProgressService extends Disposable implements IProgressService { disposables.add(dialog); disposables.add(attachDialogStyler(dialog, this.themeService)); - dialog.show().then(() => { + dialog.show().then((dialogResult) => { if (typeof onDidCancel === 'function') { - onDidCancel(); + onDidCancel(dialogResult.button); } dispose(dialog); diff --git a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts index fd979ca094c62..ea4a572bf9361 100644 --- a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts +++ b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts @@ -11,6 +11,7 @@ import { IProductService } from 'vs/platform/product/common/product'; import { IWebSocketFactory, BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ILogService } from 'vs/platform/log/common/log'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { @@ -23,12 +24,13 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ISignService signService: ISignService + @ISignService signService: ISignService, + @ILogService logService: ILogService ) { super(environmentService); this.socketFactory = new BrowserSocketFactory(webSocketFactory); - this._connection = this._register(new RemoteAgentConnection(environmentService.configuration.remoteAuthority!, productService.commit, this.socketFactory, remoteAuthorityResolverService, signService)); + this._connection = this._register(new RemoteAgentConnection(environmentService.configuration.remoteAuthority!, productService.commit, this.socketFactory, remoteAuthorityResolverService, signService, logService)); } getConnection(): IRemoteAgentConnection | null { diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 3a789e23d9855..977a224418b33 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -17,9 +17,10 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } f import { Registry } from 'vs/platform/registry/common/platform'; import { RemoteExtensionEnvironmentChannelClient } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { Emitter } from 'vs/base/common/event'; import { ISignService } from 'vs/platform/sign/common/sign'; +import { ILogService } from 'vs/platform/log/common/log'; export abstract class AbstractRemoteAgentService extends Disposable { @@ -86,7 +87,8 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon private readonly _commit: string | undefined, private readonly _socketFactory: ISocketFactory, private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, - private readonly _signService: ISignService + private readonly _signService: ISignService, + private readonly _logService: ILogService ) { super(); this.remoteAuthority = remoteAuthority; @@ -124,7 +126,8 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon return { host: authority.host, port: authority.port }; } }, - signService: this._signService + signService: this._signService, + logService: this._logService }; const connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`)); this._register(connection.onDidStateChange(e => this._onDidStateChange.fire(e))); diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index ae87e384bb406..fe15f2b644e0a 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -8,7 +8,8 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { RemoteAuthorities } from 'vs/base/common/network'; export interface IGetEnvironmentDataArguments { language: string; @@ -18,6 +19,7 @@ export interface IGetEnvironmentDataArguments { export interface IRemoteAgentEnvironmentDTO { pid: number; + connectionToken: string; appRoot: UriComponents; appSettingsHome: UriComponents; settingsPath: UriComponents; @@ -43,8 +45,11 @@ export class RemoteExtensionEnvironmentChannelClient { const data = await this.channel.call('getEnvironmentData', args); + RemoteAuthorities.setConnectionToken(remoteAuthority, data.connectionToken); + return { pid: data.pid, + connectionToken: data.connectionToken, appRoot: URI.revive(data.appRoot), appSettingsHome: URI.revive(data.appSettingsHome), settingsPath: URI.revive(data.settingsPath), diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index bc4d49641e12f..fdd61483fd5b3 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { Event } from 'vs/base/common/event'; import { PersistenConnectionEvent as PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; diff --git a/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts index 82d039a6c5405..dc3d808786a05 100644 --- a/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts +++ b/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts @@ -12,6 +12,7 @@ import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { AbstractRemoteAgentService, RemoteAgentConnection } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ILogService } from 'vs/platform/log/common/log'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { @@ -22,12 +23,13 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR constructor({ remoteAuthority }: IWindowConfiguration, @IEnvironmentService environmentService: IEnvironmentService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ISignService signService: ISignService + @ISignService signService: ISignService, + @ILogService logService: ILogService ) { super(environmentService); this.socketFactory = nodeSocketFactory; if (remoteAuthority) { - this._connection = this._register(new RemoteAgentConnection(remoteAuthority, product.commit, nodeSocketFactory, remoteAuthorityResolverService, signService)); + this._connection = this._register(new RemoteAgentConnection(remoteAuthority, product.commit, nodeSocketFactory, remoteAuthorityResolverService, signService, logService)); } } diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 457411cd670d8..1cab2997a3709 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -15,6 +15,7 @@ import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise { const tunnel = new NodeRemoteTunnel(options, tunnelRemotePort); @@ -90,7 +91,8 @@ export class TunnelService implements ITunnelService { public constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ISignService private readonly signService: ISignService + @ISignService private readonly signService: ISignService, + @ILogService private readonly logService: ILogService ) { } @@ -109,7 +111,8 @@ export class TunnelService implements ITunnelService { return { host: authority.host, port: authority.port }; } }, - signService: this.signService + signService: this.signService, + logService: this.logService }; return createRemoteTunnel(options, remotePort); } diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index cebf1654ecaa8..13450f58132cb 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRequestOptions, IRequestContext } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/services/search/common/replace.ts b/src/vs/workbench/services/search/common/replace.ts index a6caab8223e4d..99f610bbcf5ce 100644 --- a/src/vs/workbench/services/search/common/replace.ts +++ b/src/vs/workbench/services/search/common/replace.ts @@ -6,6 +6,7 @@ import * as strings from 'vs/base/common/strings'; import { IPatternInfo } from 'vs/workbench/services/search/common/search'; import { CharCode } from 'vs/base/common/charCode'; +import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; export class ReplacePattern { @@ -54,7 +55,7 @@ export class ReplacePattern { * Returns the replace string for the first match in the given text. * If text has no matches then returns null. */ - getReplaceString(text: string): string | null { + getReplaceString(text: string, preserveCase?: boolean): string | null { this._regExp.lastIndex = 0; let match = this._regExp.exec(text); if (match) { @@ -65,12 +66,20 @@ export class ReplacePattern { let replaceString = text.replace(this._regExp, this.pattern); return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length)); } - return this.pattern; + return this.buildReplaceString(match, preserveCase); } return null; } + public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string { + if (preserveCase) { + return buildReplaceStringWithCasePreserved(matches, this._replacePattern); + } else { + return this._replacePattern; + } + } + /** * \n => LF * \t => TAB diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 53de732e0111b..9b00ee68b6356 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -12,7 +12,8 @@ import { URI as uri } from 'vs/base/common/uri'; import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IDebugParams, IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IDebugParams } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { FileMatch, IFileMatch, IFileQuery, IProgressMessage, IRawSearchService, ISearchComplete, ISearchConfiguration, ISearchProgressItem, ISearchResultProvider, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, isSerializedSearchComplete, isSerializedSearchSuccess, ITextQuery, ISearchService } from 'vs/workbench/services/search/common/search'; @@ -35,7 +36,7 @@ export class LocalSearchService extends SearchService { @ILogService logService: ILogService, @IExtensionService extensionService: IExtensionService, @IFileService fileService: IFileService, - @IEnvironmentService readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService, @IInstantiationService readonly instantiationService: IInstantiationService ) { super(modelService, untitledEditorService, editorService, telemetryService, logService, extensionService, fileService); @@ -204,4 +205,4 @@ export class DiskSearch implements ISearchResultProvider { } } -registerSingleton(ISearchService, LocalSearchService, true); \ No newline at end of file +registerSingleton(ISearchService, LocalSearchService, true); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 652aaf3a2de0d..eac9dcfb1d0a7 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -15,67 +15,10 @@ import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/pla import { IStorageService } from 'vs/platform/storage/common/storage'; import { resolveWorkbenchCommonProperties } from 'vs/platform/telemetry/browser/workbenchCommonProperties'; import { IProductService } from 'vs/platform/product/common/product'; - -interface IConfig { - instrumentationKey?: string; - endpointUrl?: string; - emitLineDelimitedJson?: boolean; - accountId?: string; - sessionRenewalMs?: number; - sessionExpirationMs?: number; - maxBatchSizeInBytes?: number; - maxBatchInterval?: number; - enableDebug?: boolean; - disableExceptionTracking?: boolean; - disableTelemetry?: boolean; - verboseLogging?: boolean; - diagnosticLogInterval?: number; - samplingPercentage?: number; - autoTrackPageVisitTime?: boolean; - disableAjaxTracking?: boolean; - overridePageViewDuration?: boolean; - maxAjaxCallsPerView?: number; - disableDataLossAnalysis?: boolean; - disableCorrelationHeaders?: boolean; - correlationHeaderExcludedDomains?: string[]; - disableFlushOnBeforeUnload?: boolean; - enableSessionStorageBuffer?: boolean; - isCookieUseDisabled?: boolean; - cookieDomain?: string; - isRetryDisabled?: boolean; - url?: string; - isStorageUseDisabled?: boolean; - isBeaconApiDisabled?: boolean; - sdkExtension?: string; - isBrowserLinkTrackingEnabled?: boolean; - appId?: string; - enableCorsCorrelation?: boolean; -} - -declare class Microsoft { - public static ApplicationInsights: { - Initialization: { - new(init: { config: IConfig }): AppInsights; - } - }; -} - -declare interface IAppInsightsClient { - config: IConfig; - - /** Log a user action or other occurrence. */ - trackEvent: (name: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }) => void; - - /** Immediately send all queued telemetry. Synchronous. */ - flush(): void; -} - -interface AppInsights { - loadAppInsights: () => IAppInsightsClient; -} +import { ApplicationInsights } from '@microsoft/applicationinsights-web'; export class WebTelemetryAppender implements ITelemetryAppender { - private _aiClient?: IAppInsightsClient; + private _aiClient?: ApplicationInsights; constructor(aiKey: string, private _logService: ILogService) { const initConfig = { @@ -89,8 +32,8 @@ export class WebTelemetryAppender implements ITelemetryAppender { } }; - const appInsights = new Microsoft.ApplicationInsights.Initialization(initConfig); - this._aiClient = appInsights.loadAppInsights(); + this._aiClient = new ApplicationInsights(initConfig); + this._aiClient.loadAppInsights(); } log(eventName: string, data: any): void { @@ -101,7 +44,11 @@ export class WebTelemetryAppender implements ITelemetryAppender { data = validateTelemetryData(data); this._logService.trace(`telemetry/${eventName}`, data); - this._aiClient.trackEvent('monacoworkbench/' + eventName, data.properties, data.measurements); + this._aiClient.trackEvent({ + name: 'monacoworkbench/' + eventName, + properties: data.properties, + measurements: data.measurements + }); } flush(): Promise { @@ -167,4 +114,4 @@ export class TelemetryService extends Disposable implements ITelemetryService { } } -registerSingleton(ITelemetryService, TelemetryService); \ No newline at end of file +registerSingleton(ITelemetryService, TelemetryService); diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 6eaaf220ce572..c5561e4429580 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -69,4 +69,4 @@ export class TelemetryService extends Disposable implements ITelemetryService { } } -registerSingleton(ITelemetryService, TelemetryService); \ No newline at end of file +registerSingleton(ITelemetryService, TelemetryService); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index ac16d811705c3..ed7f5273046b5 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { ITextFileService, IResourceEncodings, IResourceEncoding } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IResourceEncodings, IResourceEncoding, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -17,15 +17,19 @@ export class BrowserTextFileService extends TextFileService { } }; - protected beforeShutdown(reason: ShutdownReason): boolean { + protected onBeforeShutdown(reason: ShutdownReason): boolean { // Web: we cannot perform long running in the shutdown phase // As such we need to check sync if there are any dirty files // that have not been backed up yet and then prevent the shutdown // if that is the case. - return this.doBeforeShutdownSync(reason); + return this.doBeforeShutdownSync(); } - private doBeforeShutdownSync(reason: ShutdownReason): boolean { + private doBeforeShutdownSync(): boolean { + if (this.models.getAll().some(model => model.hasState(ModelState.PENDING_SAVE) || model.hasState(ModelState.PENDING_AUTO_SAVE))) { + return true; // files are pending to be saved: veto + } + const dirtyResources = this.getDirty(); if (!dirtyResources.length) { return false; // no dirty: no veto diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 4fa61e19c8397..bd6332610d805 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -593,6 +593,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Create new save timer and store it for disposal as needed const handle = setTimeout(() => { + // Clear the timeout now that we are running + this.autoSaveDisposable.clear(); + // Only trigger save if the version id has not changed meanwhile if (versionId === this.versionId) { this.doSave(versionId, { reason: SaveReason.AUTO }); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change @@ -941,6 +944,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.inOrphanMode; case ModelState.PENDING_SAVE: return this.saveSequentializer.hasPendingSave(); + case ModelState.PENDING_AUTO_SAVE: + return !!this.autoSaveDisposable.value; case ModelState.SAVED: return !this.dirty; } diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 713ef13e3950a..f9147e602e805 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -107,7 +107,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer private registerListeners(): void { // Lifecycle - this.lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(event.reason))); + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); this.lifecycleService.onShutdown(this.dispose, this); // Files configuration changes @@ -118,7 +118,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer })); } - protected beforeShutdown(reason: ShutdownReason): boolean | Promise { + protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { // Dirty files need treatment on shutdown const dirty = this.getDirty(); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 02956fd1cdd18..4f770994393e6 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -248,10 +248,15 @@ export const enum ModelState { DIRTY, /** - * A model is transitioning from dirty to saved. + * A model is currently being saved but this operation has not completed yet. */ PENDING_SAVE, + /** + * A model is marked for being saved after a specific timeout. + */ + PENDING_AUTO_SAVE, + /** * A model is in conflict mode when changes cannot be saved because the * underlying file has changed. Models in conflict mode are always dirty. diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index b5609d42d322d..629e19c6b7705 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -52,6 +52,7 @@ suite('Files - TextFileEditorModel', () => { model.textEditorModel!.setValue('bar'); assert.ok(getLastModifiedTime(model) <= Date.now()); + assert.ok(model.hasState(ModelState.DIRTY)); let savedEvent = false; model.onDidStateChange(e => { @@ -60,9 +61,13 @@ suite('Files - TextFileEditorModel', () => { } }); - await model.save(); + const pendingSave = model.save(); + assert.ok(model.hasState(ModelState.PENDING_SAVE)); + + await pendingSave; assert.ok(model.getLastSaveAttemptTime() <= Date.now()); + assert.ok(model.hasState(ModelState.SAVED)); assert.ok(!model.isDirty()); assert.ok(savedEvent); diff --git a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts index 6bbdf0e42e1c6..eab223e0d4a78 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts @@ -4,16 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/workbenchTestServices'; import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { rimraf, RimRafMode, copy, readFile, exists } from 'vs/base/node/pfs'; @@ -36,13 +31,8 @@ import { detectEncodingByBOM } from 'vs/base/test/node/encoding/encoding.test'; class ServiceAccessor { constructor( - @ILifecycleService public lifecycleService: TestLifecycleService, @ITextFileService public textFileService: TestTextFileService, - @IUntitledEditorService public untitledEditorService: IUntitledEditorService, - @IWindowsService public windowsService: TestWindowsService, - @IWorkspaceContextService public contextService: TestContextService, - @IModelService public modelService: ModelServiceImpl, - @IFileService public fileService: TestFileService + @IUntitledEditorService public untitledEditorService: IUntitledEditorService ) { } } diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 3059579a64956..2512af088a0e6 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -11,7 +11,7 @@ import * as Json from 'vs/base/common/json'; import { ExtensionData, IThemeExtensionPoint, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IFileService } from 'vs/platform/files/common/files'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; -import { asDomUri } from 'vs/base/browser/dom'; +import { asCSSUrl } from 'vs/base/browser/dom'; export class FileIconThemeData implements IFileIconTheme { id: string; @@ -332,7 +332,7 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i let fonts = iconThemeDocument.fonts; if (Array.isArray(fonts)) { fonts.forEach(font => { - let src = font.src.map(l => `url('${asDomUri(resolvePath(l.path))}') format('${l.format}')`).join(', '); + let src = font.src.map(l => `${asCSSUrl(resolvePath(l.path))} format('${l.format}')`).join(', '); cssRules.push(`@font-face { src: ${src}; font-family: '${font.id}'; font-weight: ${font.weight}; font-style: ${font.style}; }`); }); cssRules.push(`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: '${fonts[0].id}'; font-size: ${fonts[0].size || '150%'}}`); @@ -343,7 +343,7 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i let definition = iconThemeDocument.iconDefinitions[defId]; if (definition) { if (definition.iconPath) { - cssRules.push(`${selectors.join(', ')} { content: ' '; background-image: url("${asDomUri(resolvePath(definition.iconPath))}"); }`); + cssRules.push(`${selectors.join(', ')} { content: ' '; background-image: ${asCSSUrl(resolvePath(definition.iconPath))}; }`); } if (definition.fontCharacter || definition.fontColor) { let body = ''; diff --git a/src/vs/workbench/services/url/browser/urlService.ts b/src/vs/workbench/services/url/browser/urlService.ts new file mode 100644 index 0000000000000..61c2278813dbc --- /dev/null +++ b/src/vs/workbench/services/url/browser/urlService.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IURLService } from 'vs/platform/url/common/url'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { ServiceIdentifier, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { AbstractURLService } from 'vs/platform/url/common/urlService'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { streamToBuffer } from 'vs/base/common/buffer'; +import { ILogService } from 'vs/platform/log/common/log'; +import { generateUuid } from 'vs/base/common/uuid'; + +export interface IURLCallbackProvider { + + /** + * Indicates that a Uri has been opened outside of VSCode. The Uri + * will be forwarded to all installed Uri handlers in the system. + */ + readonly onCallback: Event; + + /** + * Creates a Uri that - if opened in a browser - must result in + * the `onCallback` to fire. + * + * The optional `Partial` must be properly restored for + * the Uri passed to the `onCallback` handler. + * + * For example: if a Uri is to be created with `scheme:"vscode"`, + * `authority:"foo"` and `path:"bar"` the `onCallback` should fire + * with a Uri `vscode://foo/bar`. + * + * If there are additional `query` values in the Uri, they should + * be added to the list of provided `query` arguments from the + * `Partial`. + */ + create(options?: Partial): URI; +} + +export class BrowserURLService extends AbstractURLService { + + _serviceBrand!: ServiceIdentifier; + + private provider: IURLCallbackProvider; + + constructor( + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(); + + this.provider = environmentService.options && environmentService.options.urlCallbackProvider ? environmentService.options.urlCallbackProvider : instantiationService.createInstance(SelfhostURLCallbackProvider); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.provider.onCallback(uri => this.open(uri))); + } + + create(options?: Partial): URI { + return this.provider.create(options); + } +} + +class SelfhostURLCallbackProvider extends Disposable implements IURLCallbackProvider { + + static FETCH_INTERVAL = 500; // fetch every 500ms + static FETCH_TIMEOUT = 5 * 60 * 1000; // ...but stop after 5min + + static QUERY_KEYS = { + REQUEST_ID: 'vscode-requestId', + SCHEME: 'vscode-scheme', + AUTHORITY: 'vscode-authority', + PATH: 'vscode-path', + QUERY: 'vscode-query', + FRAGMENT: 'vscode-fragment' + }; + + private readonly _onCallback: Emitter = this._register(new Emitter()); + readonly onCallback: Event = this._onCallback.event; + + constructor( + @IRequestService private readonly requestService: IRequestService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + create(options?: Partial): URI { + const queryValues: Map = new Map(); + + const requestId = generateUuid(); + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId); + + const { scheme, authority, path, query, fragment } = options ? options : { scheme: undefined, authority: undefined, path: undefined, query: undefined, fragment: undefined }; + + if (scheme) { + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.SCHEME, scheme); + } + + if (authority) { + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.AUTHORITY, authority); + } + + if (path) { + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.PATH, path); + } + + if (query) { + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.QUERY, query); + } + + if (fragment) { + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.FRAGMENT, fragment); + } + + // Start to poll on the callback being fired + this.periodicFetchCallback(requestId, Date.now()); + + return this.doCreateUri('/callback', queryValues); + } + + private async periodicFetchCallback(requestId: string, startTime: number): Promise { + + // Ask server for callback results + const queryValues: Map = new Map(); + queryValues.set(SelfhostURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId); + + const result = await this.requestService.request({ + url: this.doCreateUri('/fetch-callback', queryValues).toString(true) + }, CancellationToken.None); + + // Check for callback results + const content = await streamToBuffer(result.stream); + if (content.byteLength > 0) { + try { + this._onCallback.fire(URI.revive(JSON.parse(content.toString()))); + } catch (error) { + this.logService.error(error); + } + + return; // done + } + + // Continue fetching unless we hit the timeout + if (Date.now() - startTime < SelfhostURLCallbackProvider.FETCH_TIMEOUT) { + setTimeout(() => this.periodicFetchCallback(requestId, startTime), SelfhostURLCallbackProvider.FETCH_INTERVAL); + } + } + + private doCreateUri(path: string, queryValues: Map): URI { + let query: string | undefined = undefined; + + if (queryValues) { + let index = 0; + queryValues.forEach((value, key) => { + if (!query) { + query = ''; + } + + const prefix = (index++ === 0) ? '' : '&'; + query += `${prefix}${key}=${encodeURIComponent(value)}`; + }); + } + + return URI.parse(window.location.href).with({ path, query }); + } +} + +registerSingleton(IURLService, BrowserURLService, true); diff --git a/src/vs/platform/url/electron-browser/urlService.ts b/src/vs/workbench/services/url/electron-browser/urlService.ts similarity index 74% rename from src/vs/platform/url/electron-browser/urlService.ts rename to src/vs/workbench/services/url/electron-browser/urlService.ts index 06b5d7c31f085..f1a73d383d954 100644 --- a/src/vs/platform/url/electron-browser/urlService.ts +++ b/src/vs/workbench/services/url/electron-browser/urlService.ts @@ -7,9 +7,10 @@ import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { URI } from 'vs/base/common/uri'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { URLServiceChannelClient, URLHandlerChannel } from 'vs/platform/url/node/urlIpc'; -import { URLService } from 'vs/platform/url/common/urlService'; +import { URLService } from 'vs/platform/url/node/urlService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import product from 'vs/platform/product/node/product'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class RelayURLService extends URLService implements IURLHandler { @@ -27,15 +28,21 @@ export class RelayURLService extends URLService implements IURLHandler { openerService.registerOpener(this); } - async open(uri: URI): Promise { - if (uri.scheme !== product.urlProtocol) { + async open(resource: URI, options?: { openToSide?: boolean, openExternal?: boolean }): Promise { + if (options && options.openExternal) { return false; } - return await this.urlService.open(uri); + if (resource.scheme !== product.urlProtocol) { + return false; + } + + return await this.urlService.open(resource); } handleURL(uri: URI): Promise { return super.open(uri); } } + +registerSingleton(IURLService, RelayURLService); diff --git a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts b/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts index ffed9c53d5325..622da2547a3f5 100644 --- a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts +++ b/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts @@ -209,8 +209,7 @@ export class InMemoryUserDataProvider extends Disposable implements IFileSystemP readonly onDidChangeFile: Event = this._onDidChangeFile.event; private _bufferedChanges: IFileChange[] = []; - private _fireSoonHandle?: NodeJS.Timer; - + private _fireSoonHandle?: any; watch(resource: URI, opts: IWatchOptions): IDisposable { // ignore, fires for all changes... @@ -229,4 +228,4 @@ export class InMemoryUserDataProvider extends Disposable implements IFileSystemP this._bufferedChanges.length = 0; }, 5); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts index b8ba819bba8fb..e927331f93bf8 100644 --- a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts +++ b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts @@ -47,7 +47,7 @@ suite('FileUserDataProvider', () => { userDataResource = URI.file(userDataPath).with({ scheme: Schemas.userData }); await Promise.all([pfs.mkdirp(userDataPath), pfs.mkdirp(backupsPath)]); - const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: 'workspaceId' }); + const environmentService = new BrowserWorkbenchEnvironmentService({ remoteAuthority: 'remote', workspaceId: 'workspaceId', logsPath: URI.file('logFile') }); environmentService.userRoamingDataHome = userDataResource; const userDataFileSystemProvider = new FileUserDataProvider(URI.file(userDataPath), URI.file(backupsPath), diskFileSystemProvider, environmentService); @@ -321,7 +321,7 @@ suite('FileUserDataProvider - Watching', () => { localUserDataResource = URI.file(userDataPath); userDataResource = localUserDataResource.with({ scheme: Schemas.userData }); - const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: 'workspaceId' }); + const environmentService = new BrowserWorkbenchEnvironmentService({ remoteAuthority: 'remote', workspaceId: 'workspaceId', logsPath: URI.file('logFile') }); environmentService.userRoamingDataHome = userDataResource; const userDataFileSystemProvider = new FileUserDataProvider(localUserDataResource, localBackupsResource, new TestFileSystemProvider(fileEventEmitter.event), environmentService); @@ -475,4 +475,4 @@ suite('FileUserDataProvider - Watching', () => { type: FileChangeType.DELETED }]); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index 02dbd2c785243..f2c5b0043fe9a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -631,6 +631,61 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); + test('vscode.executeCodeActionProvider passes Range to provider although Selection is passed in #77997', function () { + disposables.push(extHost.registerCodeActionProvider(nullExtensionDescription, defaultSelector, { + provideCodeActions(document, rangeOrSelection): vscode.CodeAction[] { + return [{ + command: { + arguments: [document, rangeOrSelection], + command: 'command', + title: 'command_title', + }, + kind: types.CodeActionKind.Empty.append('foo'), + title: 'title', + }]; + } + })); + + const selection = new types.Selection(0, 0, 1, 1); + + return rpcProtocol.sync().then(() => { + return commands.executeCommand('vscode.executeCodeActionProvider', model.uri, selection).then(value => { + assert.equal(value.length, 1); + const [first] = value; + assert.ok(first.command); + assert.ok(first.command!.arguments![1] instanceof types.Selection); + assert.ok(first.command!.arguments![1].isEqual(selection)); + }); + }); + }); + + test('vscode.executeCodeActionProvider results seem to be missing their `isPreferred` property #78098', function () { + disposables.push(extHost.registerCodeActionProvider(nullExtensionDescription, defaultSelector, { + provideCodeActions(document, rangeOrSelection): vscode.CodeAction[] { + return [{ + command: { + arguments: [document, rangeOrSelection], + command: 'command', + title: 'command_title', + }, + kind: types.CodeActionKind.Empty.append('foo'), + title: 'title', + isPreferred: true + }]; + } + })); + + const selection = new types.Selection(0, 0, 1, 1); + + return rpcProtocol.sync().then(() => { + return commands.executeCommand('vscode.executeCodeActionProvider', model.uri, selection).then(value => { + assert.equal(value.length, 1); + const [first] = value; + assert.equal(first.isPreferred, true); + }); + }); + }); + // --- code lens test('CodeLens, back and forth', function () { 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 4eca9307e09a9..60181c415f256 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts @@ -424,4 +424,40 @@ suite('ExtHostDiagnostics', () => { collection.set(URI.parse('test:me'), array); assert.equal(callCount, 3); // same but un-equal array }); + + test('Diagnostics created by tasks aren\'t accessible to extensions #47292', async function () { + const diags = new ExtHostDiagnostics(new class implements IMainContext { + getProxy(id: any): any { + return {}; + } + set(): any { + return null; + } + assertRegistered(): void { + + } + }); + + + // + const uri = URI.parse('foo:bar'); + const data: IMarkerData[] = [{ + message: 'message', + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + severity: 3 + }]; + + const p1 = Event.toPromise(diags.onDidChangeDiagnostics); + diags.$acceptMarkersChange([[uri, data]]); + await p1; + assert.equal(diags.getDiagnostics(uri).length, 1); + + const p2 = Event.toPromise(diags.onDidChangeDiagnostics); + diags.$acceptMarkersChange([[uri, []]]); + await p2; + assert.equal(diags.getDiagnostics(uri).length, 0); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts index 8a12127904123..fb75ee14a20a3 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts @@ -48,7 +48,7 @@ suite('ExtHostWebview', () => { assert.strictEqual(lastInvokedDeserializer, serializerB); }); - test('toWebviewResource for desktop vscode-resource scheme', () => { + test('asWebviewUri for desktop vscode-resource scheme', () => { const shape = createNoopMainThreadWebviews(); const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), { webviewCspSource: '', @@ -57,37 +57,37 @@ suite('ExtHostWebview', () => { const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); assert.strictEqual( - webview.webview.toWebviewResource(URI.parse('file:///Users/codey/file.html')).toString(), + webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(), 'vscode-resource:/Users/codey/file.html', 'Unix basic' ); assert.strictEqual( - webview.webview.toWebviewResource(URI.parse('file:///Users/codey/file.html#frag')).toString(), + webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString(), 'vscode-resource:/Users/codey/file.html#frag', 'Unix should preserve fragment' ); assert.strictEqual( - webview.webview.toWebviewResource(URI.parse('file:///Users/codey/f%20ile.html')).toString(), + webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString(), 'vscode-resource:/Users/codey/f%20ile.html', 'Unix with encoding' ); assert.strictEqual( - webview.webview.toWebviewResource(URI.parse('file://localhost/Users/codey/file.html')).toString(), + webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString(), 'vscode-resource://localhost/Users/codey/file.html', 'Unix should preserve authority' ); assert.strictEqual( - webview.webview.toWebviewResource(URI.parse('file:///c:/codey/file.txt')).toString(), + webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString(), 'vscode-resource:/c%3A/codey/file.txt', 'Windows C drive' ); }); - test('toWebviewResource for web endpoint', () => { + test('asWebviewUri for web endpoint', () => { const shape = createNoopMainThreadWebviews(); const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), { @@ -101,31 +101,31 @@ suite('ExtHostWebview', () => { } assert.strictEqual( - stripEndpointUuid(webview.webview.toWebviewResource(URI.parse('file:///Users/codey/file.html')).toString()), + stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), 'webview.contoso.com/commit///Users/codey/file.html', 'Unix basic' ); assert.strictEqual( - stripEndpointUuid(webview.webview.toWebviewResource(URI.parse('file:///Users/codey/file.html#frag')).toString()), + stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), 'webview.contoso.com/commit///Users/codey/file.html#frag', 'Unix should preserve fragment' ); assert.strictEqual( - stripEndpointUuid(webview.webview.toWebviewResource(URI.parse('file:///Users/codey/f%20ile.html')).toString()), + stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), 'webview.contoso.com/commit///Users/codey/f%20ile.html', 'Unix with encoding' ); assert.strictEqual( - stripEndpointUuid(webview.webview.toWebviewResource(URI.parse('file://localhost/Users/codey/file.html')).toString()), + stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), 'webview.contoso.com/commit//localhost/Users/codey/file.html', 'Unix should preserve authority' ); assert.strictEqual( - stripEndpointUuid(webview.webview.toWebviewResource(URI.parse('file:///c:/codey/file.txt')).toString()), + stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), 'webview.contoso.com/commit///c%3A/codey/file.txt', 'Windows C drive' ); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts index e01923222da5e..72f53943e0454 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { MarkerService } from 'vs/platform/markers/common/markerService'; import { MainThreadDiagnostics } from 'vs/workbench/api/browser/mainThreadDiagnostics'; import { URI } from 'vs/base/common/uri'; +import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; suite('MainThreadDiagnostics', function () { @@ -19,7 +20,16 @@ suite('MainThreadDiagnostics', function () { test('clear markers on dispose', function () { - let diag = new MainThreadDiagnostics(null!, markerService); + let diag = new MainThreadDiagnostics(new class implements IExtHostContext { + remoteAuthority = ''; + assertRegistered() { } + set(v: any): any { return null; } + getProxy(): any { + return { + $acceptMarkersChange() { } + }; + } + }, markerService); diag.$changeMany('foo', [[URI.file('a'), [{ code: '666', diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 0ca22ba62b88d..e25eccdd4ad0e 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -84,6 +84,8 @@ import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/n import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { NodeTextFileService } from 'vs/workbench/services/textfile/node/textFileService'; import { Schemas } from 'vs/base/common/network'; +import { IProductService } from 'vs/platform/product/common/product'; +import product from 'vs/platform/product/node/product'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -708,6 +710,10 @@ export class TestEditorGroupsService implements IEditorGroupsService { throw new Error('not implemented'); } + restoreGroup(_group: number | IEditorGroup): IEditorGroup { + throw new Error('not implemented'); + } + getSize(_group: number | IEditorGroup): { width: number, height: number } { return { width: 100, height: 100 }; } @@ -1338,12 +1344,12 @@ export class TestWindowsService implements IWindowsService { public windowCount = 1; - onWindowOpen: Event; - onWindowFocus: Event; - onWindowBlur: Event; - onWindowMaximize: Event; - onWindowUnmaximize: Event; - onRecentlyOpenedChange: Event; + readonly onWindowOpen: Event = Event.None; + readonly onWindowFocus: Event = Event.None; + readonly onWindowBlur: Event = Event.None; + readonly onWindowMaximize: Event = Event.None; + readonly onWindowUnmaximize: Event = Event.None; + readonly onRecentlyOpenedChange: Event = Event.None; isFocused(_windowId: number): Promise { return Promise.resolve(false); @@ -1631,3 +1637,5 @@ export class RemoteFileSystemProvider implements IFileSystemProvider { private toFileResource(resource: URI): URI { return resource.with({ scheme: Schemas.file, authority: '' }); } } + +export const productService: IProductService = { _serviceBrand: undefined, ...product }; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 3558c2ffef3be..c2f7f1a958182 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -52,6 +52,7 @@ import 'vs/workbench/browser/parts/views/views'; //#region --- workbench services +import 'vs/workbench/services/extensions/common/inactiveExtensionUrlHandler'; import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; @@ -73,6 +74,7 @@ import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensionManagement/common/extensionEnablementService'; import 'vs/workbench/services/notification/common/notificationService'; +import 'vs/workbench/services/extensions/common/staticExtensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; @@ -80,8 +82,6 @@ import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; -import { OpenerService } from 'vs/editor/browser/services/openerService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl'; import { MarkerDecorationsService } from 'vs/editor/common/services/markerDecorationsServiceImpl'; @@ -98,11 +98,12 @@ import { IMenuService } from 'vs/platform/actions/common/actions'; import { MenuService } from 'vs/platform/actions/common/menuService'; import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadService } from 'vs/platform/download/common/downloadService'; +import { OpenerService } from 'vs/editor/browser/services/openerService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); registerSingleton(IContextViewService, ContextViewService, true); registerSingleton(IListService, ListService, true); -registerSingleton(IOpenerService, OpenerService, true); registerSingleton(IEditorWorkerService, EditorWorkerServiceImpl); registerSingleton(IMarkerDecorationsService, MarkerDecorationsService); registerSingleton(IMarkerService, MarkerService, true); @@ -111,6 +112,7 @@ registerSingleton(IModelService, ModelServiceImpl, true); registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationService); registerSingleton(IMenuService, MenuService, true); registerSingleton(IDownloadService, DownloadService, true); +registerSingleton(IOpenerService, OpenerService, true); //#endregion @@ -126,9 +128,7 @@ import 'vs/workbench/contrib/telemetry/browser/telemetry.contribution'; // Preferences import 'vs/workbench/contrib/preferences/browser/preferences.contribution'; import 'vs/workbench/contrib/preferences/browser/keybindingsEditorContribution'; -import { IPreferencesSearchService } from 'vs/workbench/contrib/preferences/common/preferences'; -import { PreferencesSearchService } from 'vs/workbench/contrib/preferences/browser/preferencesSearch'; -registerSingleton(IPreferencesSearchService, PreferencesSearchService, true); +import 'vs/workbench/contrib/preferences/browser/preferencesSearch'; // Logs import 'vs/workbench/contrib/logs/common/logs.contribution'; @@ -194,6 +194,7 @@ import 'vs/workbench/contrib/tasks/browser/task.contribution'; // Remote import 'vs/workbench/contrib/remote/common/remote.contribution'; +import 'vs/workbench/contrib/remote/browser/remote'; // Emmet import 'vs/workbench/contrib/emmet/browser/emmet.contribution'; @@ -230,4 +231,10 @@ import 'vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution'; // Outline import 'vs/workbench/contrib/outline/browser/outline.contribution'; +// Experiments +import 'vs/workbench/contrib/experiments/browser/experiments.contribution'; + +// Send a Smile +import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index cb1fd2b2a2ee7..27ef543f04065 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -20,8 +20,8 @@ import 'vs/workbench/workbench.common.main'; //#region --- workbench (desktop main) -import 'vs/workbench/electron-browser/main.contribution'; -import 'vs/workbench/electron-browser/main'; +import 'vs/workbench/electron-browser/desktop.contribution'; +import 'vs/workbench/electron-browser/desktop.main'; //#endregion @@ -30,9 +30,7 @@ import 'vs/workbench/electron-browser/main'; import 'vs/workbench/services/integrity/node/integrityService'; import 'vs/workbench/services/textMate/electron-browser/textMateService'; import 'vs/workbench/services/workspace/electron-browser/workspaceEditingService'; -import 'vs/workbench/services/extensions/common/inactiveExtensionUrlHandler'; import 'vs/workbench/services/search/node/searchService'; -import 'vs/workbench/contrib/debug/electron-browser/extensionHostDebugService'; import 'vs/workbench/services/output/node/outputChannelModelService'; import 'vs/workbench/services/textfile/node/textFileService'; import 'vs/workbench/services/dialogs/electron-browser/dialogService'; @@ -48,8 +46,9 @@ import 'vs/workbench/services/configurationResolver/electron-browser/configurati import 'vs/workbench/services/extensionManagement/node/extensionManagementService'; import 'vs/workbench/services/accessibility/node/accessibilityService'; import 'vs/workbench/services/remote/node/tunnelService'; -import 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; import 'vs/workbench/services/backup/node/backupFileService'; +import 'vs/workbench/services/credentials/node/credentialsService'; +import 'vs/workbench/services/url/electron-browser/urlService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -61,36 +60,27 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; import { LocalizationsService } from 'vs/platform/localizations/electron-browser/localizationsService'; import { ISharedProcessService, SharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { IProductService } from 'vs/platform/product/common/product'; -import { ProductService } from 'vs/platform/product/node/productService'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { WindowsService } from 'vs/platform/windows/electron-browser/windowsService'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateService } from 'vs/platform/update/electron-browser/updateService'; -import { IIssueService } from 'vs/platform/issue/common/issue'; +import { IIssueService } from 'vs/platform/issue/node/issue'; import { IssueService } from 'vs/platform/issue/electron-browser/issueService'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { WorkspacesService } from 'vs/platform/workspaces/electron-browser/workspacesService'; -import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { IMenubarService } from 'vs/platform/menubar/node/menubar'; import { MenubarService } from 'vs/platform/menubar/electron-browser/menubarService'; -import { IURLService } from 'vs/platform/url/common/url'; -import { RelayURLService } from 'vs/platform/url/electron-browser/urlService'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; registerSingleton(IClipboardService, ClipboardService, true); registerSingleton(IRequestService, RequestService, true); registerSingleton(ILifecycleService, LifecycleService); registerSingleton(ILocalizationsService, LocalizationsService); registerSingleton(ISharedProcessService, SharedProcessService, true); -registerSingleton(IProductService, ProductService, true); registerSingleton(IWindowsService, WindowsService); registerSingleton(IUpdateService, UpdateService); registerSingleton(IIssueService, IssueService); registerSingleton(IWorkspacesService, WorkspacesService); registerSingleton(IMenubarService, MenubarService); -registerSingleton(IURLService, RelayURLService); -registerSingleton(ICredentialsService, KeytarCredentialsService, true); //#endregion @@ -100,10 +90,8 @@ registerSingleton(ICredentialsService, KeytarCredentialsService, true); // Localizations import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; -// Logs -import 'vs/workbench/contrib/logs/electron-browser/logs.contribution'; - // Stats +import 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; import 'vs/workbench/contrib/stats/electron-browser/stats.contribution'; // Rapid Render Splash @@ -111,6 +99,7 @@ import 'vs/workbench/contrib/splash/electron-browser/partsSplash.contribution'; // Debug import 'vs/workbench/contrib/debug/node/debugHelperService'; +import 'vs/workbench/contrib/debug/electron-browser/extensionHostDebugService'; // Webview import 'vs/workbench/contrib/webview/electron-browser/webview.contribution'; @@ -130,9 +119,6 @@ import 'vs/workbench/contrib/codeEditor/electron-browser/codeEditor.contribution // Execution import 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; -// Send a Smile -import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; - // Update import 'vs/workbench/contrib/update/electron-browser/update.contribution'; @@ -153,9 +139,6 @@ import 'vs/workbench/contrib/themes/test/electron-browser/themes.test.contributi import 'vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.contribution'; import 'vs/workbench/contrib/welcome/page/browser/welcomePage.contribution'; -// Experiments -import 'vs/workbench/contrib/experiments/electron-browser/experiments.contribution'; - // Issues import 'vs/workbench/contrib/issue/electron-browser/issue.contribution'; diff --git a/src/vs/workbench/workbench.web.main.css b/src/vs/workbench/workbench.web.api.css similarity index 94% rename from src/vs/workbench/workbench.web.main.css rename to src/vs/workbench/workbench.web.api.css index 06ed8a197b2a3..3a0641938d56e 100644 --- a/src/vs/workbench/workbench.web.main.css +++ b/src/vs/workbench/workbench.web.api.css @@ -4,6 +4,3 @@ *--------------------------------------------------------------------------------------------*/ /* NOTE: THIS FILE WILL BE OVERWRITTEN DURING BUILD TIME, DO NOT EDIT */ - -div.monaco.main.css { -} \ No newline at end of file diff --git a/src/vs/workbench/workbench.web.main.nls.js b/src/vs/workbench/workbench.web.api.nls.js similarity index 96% rename from src/vs/workbench/workbench.web.main.nls.js rename to src/vs/workbench/workbench.web.api.nls.js index d6a8b487eaf92..6113d093d5cce 100644 --- a/src/vs/workbench/workbench.web.main.nls.js +++ b/src/vs/workbench/workbench.web.api.nls.js @@ -4,5 +4,3 @@ *--------------------------------------------------------------------------------------------*/ // NOTE: THIS FILE WILL BE OVERWRITTEN DURING BUILD TIME, DO NOT EDIT - -define([], {}); \ No newline at end of file diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index bc4b9d1d7672a..4bdfb542d9f3b 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -8,6 +8,10 @@ import { main } from 'vs/workbench/browser/web.main'; import { UriComponents } from 'vs/base/common/uri'; import { IFileSystemProvider } from 'vs/platform/files/common/files'; import { IWebSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { ICredentialsProvider } from 'vs/workbench/services/credentials/browser/credentialsService'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; +import { IProductConfiguration } from 'vs/platform/product/common/product'; export interface IWorkbenchConstructionOptions { @@ -48,6 +52,31 @@ export interface IWorkbenchConstructionOptions { * A factory for web sockets. */ webSocketFactory?: IWebSocketFactory; + + /** + * Experimental: Whether to enable the smoke test driver. + */ + driver?: boolean; + + /** + * Experimental: The credentials provider to store and retrieve secrets. + */ + credentialsProvider?: ICredentialsProvider; + + /** + * Experimental: Add static extensions that cannot be uninstalled but only be disabled. + */ + staticExtensions?: { packageJSON: IExtensionManifest, extensionLocation: UriComponents }[]; + + /** + * Experimental: Support for URL callbacks. + */ + urlCallbackProvider?: IURLCallbackProvider; + + /** + * Experimental: Support for product configuration. + */ + productConfiguration?: IProductConfiguration; } /** diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 1a41fc77ac4db..681fc606b6a86 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -26,6 +26,7 @@ import 'vs/workbench/browser/web.main'; //#region --- workbench services +import 'vs/workbench/services/integrity/browser/integrityService'; import 'vs/workbench/services/textMate/browser/textMateService'; import 'vs/workbench/services/search/common/searchService'; import 'vs/workbench/services/output/common/outputChannelModelService'; @@ -35,6 +36,8 @@ import 'vs/workbench/services/extensions/browser/extensionService'; import 'vs/workbench/services/extensionManagement/common/extensionManagementServerService'; import 'vs/workbench/services/telemetry/browser/telemetryService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import 'vs/workbench/services/credentials/browser/credentialsService'; +import 'vs/workbench/services/url/browser/urlService'; import 'vs/workbench/browser/web.simpleservices'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/test/electron/renderer.html b/test/electron/renderer.html index 724543895977f..a2d4bcc5ad9fb 100644 --- a/test/electron/renderer.html +++ b/test/electron/renderer.html @@ -11,6 +11,18 @@