diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 7520dee2091..df0fad7f826 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -65,7 +65,7 @@ This project incorporates components from the projects listed below. The origina 58. TypeScript-TmLanguage version 1.0.0 (https://github.com/Microsoft/TypeScript-TmLanguage) 59. Unicode version 12.0.0 (http://www.unicode.org/) 60. vscode-logfile-highlighter version 2.4.1 (https://github.com/emilast/vscode-logfile-highlighter) -61. vscode-octicons-font version 1.2.0 (https://github.com/Microsoft/vscode-octicons-font) +61. vscode-octicons-font version 1.3.0 (https://github.com/Microsoft/vscode-octicons-font) 62. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) 63. Web Background Synchronization (https://github.com/WICG/BackgroundSync) diff --git a/build/azure-pipelines/common/installDistro.ts b/build/azure-pipelines/common/installDistro.ts deleted file mode 100644 index 57fcadb8d3f..00000000000 --- a/build/azure-pipelines/common/installDistro.ts +++ /dev/null @@ -1,20 +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 cp from 'child_process'; -import * as path from 'path'; - -function yarnInstall(packageName: string): void { - cp.execSync(`yarn add --no-lockfile ${packageName}`); - cp.execSync(`yarn add --no-lockfile ${packageName}`, { cwd: path.join( process.cwd(), 'remote') }); -} - -const product = require('../../../product.json'); -const dependencies = product.dependencies || {} as { [name: string]: string; }; - -Object.keys(dependencies).forEach(name => { - const url = dependencies[name]; - yarnInstall(url); -}); \ No newline at end of file diff --git a/build/azure-pipelines/common/installDistroDependencies.ts b/build/azure-pipelines/common/installDistroDependencies.ts new file mode 100644 index 00000000000..a0dd3a295db --- /dev/null +++ b/build/azure-pipelines/common/installDistroDependencies.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; + +function yarnInstall(packageName: string, cwd: string): void { + console.log(`yarn add --no-lockfile ${packageName}`, cwd); + cp.execSync(`yarn add --no-lockfile ${packageName}`, { cwd, stdio: 'inherit' }); +} + +/** + * Install additional dependencies listed on each quality `package.json` file. + */ +function main() { + const quality = process.env['VSCODE_QUALITY']; + + if (!quality) { + throw new Error('Missing VSCODE_QUALITY, can\'t install distro'); + } + + const rootPath = path.dirname(path.dirname(path.dirname(__dirname))); + const qualityPath = path.join(rootPath, 'quality', quality); + const packagePath = path.join(qualityPath, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + const dependencies = pkg.dependencies || {} as { [name: string]: string; }; + + Object.keys(dependencies).forEach(name => { + const url = dependencies[name]; + const cwd = process.argv.length < 3 ? process.cwd() : path.join(process.cwd(), process.argv[2]); + yarnInstall(url, cwd); + }); +} + +main(); \ No newline at end of file diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 5d6ec8c2cff..f136f2b4496 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -34,7 +34,8 @@ steps: yarn gulp mixin yarn gulp hygiene yarn monaco-compile-check - node build/azure-pipelines/common/installDistro.js + node build/azure-pipelines/common/installDistroDependencies.js + node build/azure-pipelines/common/installDistroDependencies.js remote node build/lib/builtInExtensions.js displayName: Prepare build diff --git a/build/azure-pipelines/linux/build-arm.sh b/build/azure-pipelines/linux/build-arm.sh new file mode 100755 index 00000000000..4f01bc9a7e5 --- /dev/null +++ b/build/azure-pipelines/linux/build-arm.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +echo 'noop' \ No newline at end of file diff --git a/build/azure-pipelines/linux/prebuild-arm.sh b/build/azure-pipelines/linux/prebuild-arm.sh new file mode 100755 index 00000000000..4f01bc9a7e5 --- /dev/null +++ b/build/azure-pipelines/linux/prebuild-arm.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +echo 'noop' \ No newline at end of file diff --git a/build/azure-pipelines/linux/product-build-linux-arm.yml b/build/azure-pipelines/linux/product-build-linux-arm.yml new file mode 100644 index 00000000000..55350cce295 --- /dev/null +++ b/build/azure-pipelines/linux/product-build-linux-arm.yml @@ -0,0 +1,65 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "10.15.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.10.1" + +- task: AzureKeyVault@1 + displayName: 'Azure Key Vault: Get Secrets' + inputs: + azureSubscription: 'vscode-builds-subscription' + KeyVaultName: vscode + +- task: Docker@1 + displayName: 'Pull image' + inputs: + azureSubscriptionEndpoint: 'vscode-builds-subscription' + azureContainerRegistry: vscodehub.azurecr.io + command: 'Run an image' + imageName: 'vscode-linux-build-agent:armhf' + containerCommand: uname + +- script: | + set -e + + cat << EOF > ~/.netrc + machine monacotools.visualstudio.com + password $(devops-pat) + machine github.com + login vscode + password $(github-distro-mixin-password) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" + git fetch distro + git merge $(node -p "require('./package.json').distro") + + CHILD_CONCURRENCY=1 yarn + yarn gulp mixin + yarn gulp hygiene + yarn monaco-compile-check + ./build/azure-pipelines/linux/prebuild-arm.sh + displayName: Prepare build + +- script: | + set -e + ./build/azure-pipelines/linux/build-arm.sh + displayName: Build + +- script: | + set -e + AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + VSCODE_HOCKEYAPP_TOKEN="$(vscode-hockeyapp-token)" \ + ./build/azure-pipelines/linux/publish-arm.sh + displayName: Publish + +- task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: 'Component Detection' + continueOnError: true \ No newline at end of file diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index f727e30d252..5ea5de8ade1 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -38,7 +38,8 @@ steps: yarn gulp mixin yarn gulp hygiene yarn monaco-compile-check - node build/azure-pipelines/common/installDistro.js + node build/azure-pipelines/common/installDistroDependencies.js + node build/azure-pipelines/common/installDistroDependencies.js remote node build/lib/builtInExtensions.js displayName: Prepare build diff --git a/build/azure-pipelines/linux/publish-arm.sh b/build/azure-pipelines/linux/publish-arm.sh new file mode 100755 index 00000000000..4f01bc9a7e5 --- /dev/null +++ b/build/azure-pipelines/linux/publish-arm.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +echo 'noop' \ No newline at end of file diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index c8bedfbffc0..8b013cb9211 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -1,11 +1,11 @@ resources: containers: - container: vscode-x64 - endpoint: VSCodeHub image: vscodehub.azurecr.io/vscode-linux-build-agent:x64 - - container: vscode-ia32 endpoint: VSCodeHub + - container: vscode-ia32 image: vscodehub.azurecr.io/vscode-linux-build-agent:ia32 + endpoint: VSCodeHub - container: snapcraft image: snapcore/snapcraft @@ -59,6 +59,15 @@ jobs: steps: - template: linux/product-build-linux.yml +- job: LinuxArmhf + condition: eq(variables['VSCODE_BUILD_LINUX_ARMHF'], 'true') + pool: + vmImage: 'Ubuntu-16.04' + variables: + VSCODE_ARCH: armhf + steps: + - template: linux/product-build-linux-arm.yml + - job: macOS condition: eq(variables['VSCODE_BUILD_MACOS'], 'true') pool: @@ -76,6 +85,7 @@ jobs: - Linux - LinuxSnap - Linux32 + - LinuxArmhf - macOS steps: - template: sync-mooncake.yml \ No newline at end of file diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 8a6a6abb658..339bdde1b9c 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -35,7 +35,8 @@ steps: exec { yarn gulp mixin } exec { yarn gulp hygiene } exec { yarn monaco-compile-check } - exec { node build/azure-pipelines/common/installDistro.js } + exec { node build/azure-pipelines/common/installDistroDependencies.js } + exec { node build/azure-pipelines/common/installDistroDependencies.js remote } exec { node build/lib/builtInExtensions.js } displayName: Prepare build diff --git a/build/gulpfile.mixin.js b/build/gulpfile.mixin.js index 690cc874899..0f9b1d990f3 100644 --- a/build/gulpfile.mixin.js +++ b/build/gulpfile.mixin.js @@ -27,7 +27,8 @@ gulp.task('mixin', function () { fancyLog(ansiColors.blue('[mixin]'), `Mixing in sources:`); return vfs .src(`quality/${quality}/**`, { base: `quality/${quality}` }) - .pipe(filter(function (f) { return !f.isDirectory(); })) + .pipe(filter(f => !f.isDirectory())) + .pipe(filter(['**', '!**/package.json'])) .pipe(productJsonFilter) .pipe(buffer()) .pipe(json(o => Object.assign({}, require('../product.json'), o))) diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index d7361082a89..8e2c4223bf0 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -13,4 +13,4 @@ gulp.task('vscode-reh-win32-ia32-min', noop); gulp.task('vscode-reh-win32-x64-min', noop); gulp.task('vscode-reh-darwin-min', noop); gulp.task('vscode-reh-linux-x64-min', noop); -gulp.task('vscode-reh-linux-arm-min', noop); +gulp.task('vscode-reh-linux-armhf-min', noop); diff --git a/extensions/css/cgmanifest.json b/extensions/css/cgmanifest.json index 7b7a8d5e8e8..a47fef967ec 100644 --- a/extensions/css/cgmanifest.json +++ b/extensions/css/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "octref/language-css", "repositoryUrl": "https://github.com/octref/language-css", - "commitHash": "6d3a2d01dd67ef062030f4520dd42a5424330a3b" + "commitHash": "377734aad976be88a425aab5667784f3f71ea7e5" } }, "license": "MIT", diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index a45463c8b9b..8dbdf1f7413 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/octref/language-css/commit/6d3a2d01dd67ef062030f4520dd42a5424330a3b", + "version": "https://github.com/octref/language-css/commit/377734aad976be88a425aab5667784f3f71ea7e5", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -606,8 +606,32 @@ ] }, { - "begin": "(?i)(?=@[\\w-]+(\\s|\\(|{|;|/\\*|$))", - "end": "(?<=}|;)(?!\\G)", + "begin": "(?i)(?=@[\\w-]+[^;]+;s*$)", + "end": "(?<=;)(?!\\G)", + "patterns": [ + { + "begin": "(?i)\\G(@)[\\w-]+", + "beginCaptures": { + "0": { + "name": "keyword.control.at-rule.css" + }, + "1": { + "name": "punctuation.definition.keyword.css" + } + }, + "end": ";", + "endCaptures": { + "0": { + "name": "punctuation.terminator.rule.css" + } + }, + "name": "meta.at-rule.header.css" + } + ] + }, + { + "begin": "(?i)(?=@[\\w-]+(\\s|\\(|{|/\\*|$))", + "end": "(?<=})(?!\\G)", "patterns": [ { "begin": "(?i)\\G(@)[\\w-]+", diff --git a/extensions/fsharp/cgmanifest.json b/extensions/fsharp/cgmanifest.json index d24f6a3d010..b093d908572 100644 --- a/extensions/fsharp/cgmanifest.json +++ b/extensions/fsharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "ionide/ionide-fsgrammar", "repositoryUrl": "https://github.com/ionide/ionide-fsgrammar", - "commitHash": "b2100c95d7857c5421d111a860fcdd20954a0263" + "commitHash": "b57388e5a0971412c081cf0cea8b50b9c24ea4e8" } }, "license": "MIT", diff --git a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json index 82e6d33548f..82bd02d54ab 100644 --- a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json +++ b/extensions/fsharp/syntaxes/fsharp.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/ionide/ionide-fsgrammar/commit/b2100c95d7857c5421d111a860fcdd20954a0263", + "version": "https://github.com/ionide/ionide-fsgrammar/commit/b57388e5a0971412c081cf0cea8b50b9c24ea4e8", "name": "fsharp", "scopeName": "source.fsharp", "patterns": [ @@ -335,6 +335,44 @@ } ] }, + "anonymous_record_declaration": { + "begin": "(\\{\\|)", + "end": "(\\|\\})", + "beginCaptures": { + "1": { + "name": "keyword.symbol.fsharp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.symbol.fsharp" + } + }, + "patterns": [ + { + "match": "[[:alpha:]0-9'`^_ ]+(:)", + "captures": { + "1": { + "name": "keyword.symbol.fsharp" + } + } + }, + { + "match": "([[:alpha:]0-9'`^_ ]+)", + "captures": { + "1": { + "name": "entity.name.type.fsharp" + } + } + }, + { + "include": "#anonymous_record_declaration" + }, + { + "include": "#keywords" + } + ] + }, "record_signature": { "patterns": [ { @@ -819,6 +857,9 @@ } ] }, + { + "include": "#anonymous_record_declaration" + }, { "begin": "({)", "end": "(})", @@ -982,6 +1023,9 @@ } } }, + { + "include": "#anonymous_record_declaration" + }, { "include": "#keywords" } @@ -1062,6 +1106,9 @@ "name": "entity.name.section.fsharp" } } + }, + { + "include": "#comments" } ] }, @@ -1255,6 +1302,9 @@ } } }, + { + "include": "#anonymous_record_declaration" + }, { "begin": "(\\?{0,1})([[:alpha:]0-9'`^._ ]+)\\s*(:)(\\s*([?[:alpha:]0-9'`^._ ]+)(<))", "end": "(>)", diff --git a/extensions/make/cgmanifest.json b/extensions/make/cgmanifest.json index ed80096c8ce..c5a402aa39e 100644 --- a/extensions/make/cgmanifest.json +++ b/extensions/make/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "fadeevab/make.tmbundle", "repositoryUrl": "https://github.com/fadeevab/make.tmbundle", - "commitHash": "bd71f44ea55d61be711bd7676e600a7333cc79ea" + "commitHash": "1b05209b483f81f42270bdda5514590e013e4896" } }, "licenseDetail": [ diff --git a/extensions/make/syntaxes/make.tmLanguage.json b/extensions/make/syntaxes/make.tmLanguage.json index f2e43e163ee..bdb08837c26 100644 --- a/extensions/make/syntaxes/make.tmLanguage.json +++ b/extensions/make/syntaxes/make.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/fadeevab/make.tmbundle/commit/bd71f44ea55d61be711bd7676e600a7333cc79ea", + "version": "https://github.com/fadeevab/make.tmbundle/commit/1b05209b483f81f42270bdda5514590e013e4896", "name": "Makefile", "scopeName": "source.makefile", "patterns": [ @@ -132,6 +132,9 @@ }, { "include": "#comment" + }, + { + "include": "#directives" } ] }, diff --git a/extensions/rust/cgmanifest.json b/extensions/rust/cgmanifest.json index c3056722ece..5181efba1c4 100644 --- a/extensions/rust/cgmanifest.json +++ b/extensions/rust/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-rust", "repositoryUrl": "https://github.com/zargony/atom-language-rust", - "commitHash": "5238d9834953ed7c58d9b5b9bb0c084c3c11ecd6" + "commitHash": "7d59e2ad79fbe5925bd2fd3bd3857bf9f421ff6f" } }, "license": "MIT", diff --git a/extensions/rust/syntaxes/rust.tmLanguage.json b/extensions/rust/syntaxes/rust.tmLanguage.json index 9b7373336df..784bd8c9aca 100644 --- a/extensions/rust/syntaxes/rust.tmLanguage.json +++ b/extensions/rust/syntaxes/rust.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/zargony/atom-language-rust/commit/5238d9834953ed7c58d9b5b9bb0c084c3c11ecd6", + "version": "https://github.com/zargony/atom-language-rust/commit/7d59e2ad79fbe5925bd2fd3bd3857bf9f421ff6f", "name": "Rust", "scopeName": "source.rust", "patterns": [ @@ -160,7 +160,7 @@ { "comment": "Control keyword", "name": "keyword.control.rust", - "match": "\\b(break|continue|else|if|in|for|loop|match|return|while)\\b" + "match": "\\b(async|await|break|continue|else|if|in|for|loop|match|return|try|while)\\b" }, { "comment": "Keyword", diff --git a/extensions/typescript-language-features/src/features/diagnostics.ts b/extensions/typescript-language-features/src/features/diagnostics.ts index a9de15ab539..6e0568824ad 100644 --- a/extensions/typescript-language-features/src/features/diagnostics.ts +++ b/extensions/typescript-language-features/src/features/diagnostics.ts @@ -6,6 +6,25 @@ import * as vscode from 'vscode'; import { ResourceMap } from '../utils/resourceMap'; import { DiagnosticLanguage, allDiagnosticLanguages } from '../utils/languageDescription'; +import * as arrays from '../utils/arrays'; + +function diagnosticsEquals(a: vscode.Diagnostic, b: vscode.Diagnostic): boolean { + if (a === b) { + return true; + } + + return a.code === b.code + && a.message === b.message + && a.severity === b.severity + && a.source === b.source + && a.range.isEqual(b.range) + && arrays.equals(a.relatedInformation || arrays.empty, b.relatedInformation || arrays.empty, (a, b) => { + return a.message === b.message + && a.location.range.isEqual(b.location.range) + && a.location.uri.fsPath === b.location.uri.fsPath; + }) + && arrays.equals(a.tags || arrays.empty, b.tags || arrays.empty); +} export const enum DiagnosticKind { Syntax, @@ -31,12 +50,10 @@ class FileDiagnostics { this.language = language; } - if (diagnostics.length === 0) { - const existing = this._diagnostics.get(kind); - if (!existing || existing && existing.length === 0) { - // No need to update - return false; - } + const existing = this._diagnostics.get(kind); + if (arrays.equals(existing || arrays.empty, diagnostics, diagnosticsEquals)) { + // No need to update + return false; } this._diagnostics.set(kind, diagnostics); diff --git a/extensions/typescript-language-features/src/utils/arrays.ts b/extensions/typescript-language-features/src/utils/arrays.ts index bf403c3afeb..f9435fe9b20 100644 --- a/extensions/typescript-language-features/src/utils/arrays.ts +++ b/extensions/typescript-language-features/src/utils/arrays.ts @@ -2,20 +2,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { - if (one.length !== other.length) { + +export const empty = Object.freeze([]); + +export function equals( + a: ReadonlyArray, + b: ReadonlyArray, + itemEquals: (a: T, b: T) => boolean = (a, b) => a === b +): boolean { + if (a === b) { + return true; + } + if (a.length !== b.length) { return false; } - - for (let i = 0, len = one.length; i < len; i++) { - if (!itemEquals(one[i], other[i])) { - return false; - } - } - - return true; + return a.every((x, i) => itemEquals(x, b[i])); } export function flatten(arr: ReadonlyArray[]): T[] { - return ([] as T[]).concat.apply([], arr); + return Array.prototype.concat.apply([], arr); } \ No newline at end of file diff --git a/package.json b/package.json index c0ebd1a6121..0ef3d543203 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.35.0", - "distro": "78b820fcfbca46e0a1387efef3b8216e04100f45", + "version": "1.36.0", + "distro": "15aab9a6b297eb15ea96c450d3b92b0758572cca", "author": { "name": "Microsoft Corporation" }, @@ -155,4 +155,4 @@ "windows-mutex": "0.2.1", "windows-process-tree": "0.2.3" } -} +} \ No newline at end of file diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 966555e04d0..134573e9389 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -96,6 +96,16 @@ const _empty = ''; const _slash = '/'; const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; +function _isQueryStringScheme(scheme: string) { + switch (scheme.toLowerCase()) { + case 'http': + case 'https': + case 'ftp': + return true; + } + return false; +} + /** * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. * This class is a simple parser which creates the basic component parts @@ -282,14 +292,14 @@ export class URI implements UriComponents { static parse(value: string, _strict: boolean = false): URI { const match = _regexp.exec(value); if (!match) { - return new _URI(_empty, _empty, _empty, _empty, _empty); + return new _URI(_empty, _empty, _empty, _empty, _empty, _strict); } return new _URI( match[2] || _empty, - decodeURIComponent(match[4] || _empty), - decodeURIComponent(match[5] || _empty), - decodeURIComponent(match[7] || _empty), - decodeURIComponent(match[9] || _empty), + decodeURIComponentFast(match[4] || _empty, false, false), + decodeURIComponentFast(match[5] || _empty, true, false), + decodeURIComponentFast(match[7] || _empty, false, _isQueryStringScheme(match[2])), + decodeURIComponentFast(match[9] || _empty, false, false), _strict ); } @@ -465,6 +475,84 @@ class _URI extends URI { } } +function isHex(value: string, pos: number): boolean { + if (pos >= value.length) { + return false; + } + const code = value.charCodeAt(pos); + return (code >= CharCode.Digit0 && code <= CharCode.Digit9)// 0-9 + || (code >= CharCode.a && code <= CharCode.f) //a-f + || (code >= CharCode.A && code <= CharCode.F); //A-F +} + + +function decodeURIComponentFast(uriComponent: string, isPath: boolean, isQueryString: boolean): string { + + let res: string | undefined; + let nativeDecodePos = -1; + + for (let pos = 0; pos < uriComponent.length; pos++) { + const code = uriComponent.charCodeAt(pos); + + // decoding needed + if (code === CharCode.PercentSign && isHex(uriComponent, pos + 1) && isHex(uriComponent, pos + 2)) { + + const chA = uriComponent.charCodeAt(pos + 1); + const chB = uriComponent.charCodeAt(pos + 2); + + // when in a path -> check and accept %2f and %2F (fwd slash) + // when in a query string -> check and accept %3D, %26, and %3B (equals, ampersand, semi-colon) + if ( + (isPath && chA === CharCode.Digit2 && (chB === CharCode.F || chB === CharCode.f)) + || + (isQueryString && ( + (chA === CharCode.Digit2 && chB === CharCode.Digit6) // %26 + || + (chA === CharCode.Digit3 && (chB === CharCode.B || chB === CharCode.b || chB === CharCode.D || chB === CharCode.d)) // %3D, %3D + )) + ) { + if (nativeDecodePos !== -1) { + res += decodeURIComponent(uriComponent.substring(nativeDecodePos, pos)); + nativeDecodePos = -1; + } + + if (res !== undefined) { + res += uriComponent.substr(pos, 3); + } + + pos += 2; + continue; + } + + if (res === undefined) { + res = uriComponent.substring(0, pos); + } + if (nativeDecodePos === -1) { + nativeDecodePos = pos; + } + + pos += 2; + + } else { + + if (nativeDecodePos !== -1) { + res += decodeURIComponent(uriComponent.substring(nativeDecodePos, pos)); + nativeDecodePos = -1; + } + + if (res !== undefined) { + res += String.fromCharCode(code); + } + } + } + + if (nativeDecodePos !== -1) { + res += decodeURIComponent(uriComponent.substr(nativeDecodePos)); + } + + return res !== undefined ? res : uriComponent; +} + // reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2 const encodeTable: { [ch: number]: string } = { [CharCode.Colon]: '%3A', // gen-delims @@ -490,7 +578,7 @@ const encodeTable: { [ch: number]: string } = { [CharCode.Space]: '%20', }; -function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): string { +function encodeURIComponentFast(uriComponent: string, isPath: boolean, isQueryString: boolean): string { let res: string | undefined = undefined; let nativeEncodePos = -1; @@ -506,7 +594,8 @@ function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): stri || code === CharCode.Period || code === CharCode.Underline || code === CharCode.Tilde - || (allowSlash && code === CharCode.Slash) + || (isPath && code === CharCode.Slash) // path => allow slash AS-IS + || (isQueryString && (code === CharCode.Equals || code === CharCode.Ampersand || code === CharCode.Semicolon)) // query string => allow &=; ) { // check if we are delaying native encode if (nativeEncodePos !== -1) { @@ -518,6 +607,20 @@ function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): stri res += uriComponent.charAt(pos); } + } else if (code === CharCode.PercentSign && isHex(uriComponent, pos + 1) && isHex(uriComponent, pos + 2)) { + // at percentage encoded value + + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.substr(pos, 3); + } + pos += 2; + } else { // encoding needed, we need to allocate a new string if (res === undefined) { @@ -606,6 +709,7 @@ function _asFormatted(uri: URI, skipEncoding: boolean): string { let res = ''; let { scheme, authority, path, query, fragment } = uri; + if (scheme) { res += scheme; res += ':'; @@ -622,22 +726,22 @@ function _asFormatted(uri: URI, skipEncoding: boolean): string { authority = authority.substr(idx + 1); idx = userinfo.indexOf(':'); if (idx === -1) { - res += encoder(userinfo, false); + res += encoder(userinfo, false, false); } else { // :@ - res += encoder(userinfo.substr(0, idx), false); + res += encoder(userinfo.substr(0, idx), false, false); res += ':'; - res += encoder(userinfo.substr(idx + 1), false); + res += encoder(userinfo.substr(idx + 1), false, false); } res += '@'; } authority = authority.toLowerCase(); idx = authority.indexOf(':'); if (idx === -1) { - res += encoder(authority, false); + res += encoder(authority, false, false); } else { // : - res += encoder(authority.substr(0, idx), false); + res += encoder(authority.substr(0, idx), false, false); res += authority.substr(idx); } } @@ -655,15 +759,15 @@ function _asFormatted(uri: URI, skipEncoding: boolean): string { } } // encode the rest of the path - res += encoder(path, true); + res += encoder(path, true, false); } if (query) { res += '?'; - res += encoder(query, false); + res += encoder(query, false, _isQueryStringScheme(scheme)); } if (fragment) { res += '#'; - res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; + res += !skipEncoding ? encodeURIComponentFast(fragment, false, false) : fragment; } return res; } diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts index a221a94c867..fe8d7538d85 100644 --- a/src/vs/base/node/config.ts +++ b/src/vs/base/node/config.ts @@ -109,10 +109,10 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { try { this.parseErrors = []; res = this.options.parse ? this.options.parse(raw, this.parseErrors) : json.parse(raw, this.parseErrors); + return res || this.options.defaultConfig; } catch (error) { - // Ignore parsing errors - return this.options.defaultConfig; + return this.options.defaultConfig; // Ignore parsing errors } } diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 85f2ad691ac..a4495718fcf 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -63,7 +63,7 @@ suite('URI', () => { assert.equal(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); assert.equal(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); //http://a-test-site.com/#test=true - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); + assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test=true'); assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); }); @@ -102,11 +102,11 @@ suite('URI', () => { test('with, changes', () => { assert.equal(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t=1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t=1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t=1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t=1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t=1234'); assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); }); @@ -262,11 +262,11 @@ suite('URI', () => { value = URI.file('c:\\test with %25\\path'); assert.equal(value.path, '/c:/test with %25/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); + assert.equal(value.toString(), 'file:///c%3A/test%20with%20%25/path'); value = URI.file('c:\\test with %25\\c#code'); assert.equal(value.path, '/c:/test with %25/c#code'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); + assert.equal(value.toString(), 'file:///c%3A/test%20with%20%25/c%23code'); value = URI.file('\\\\shares'); assert.equal(value.scheme, 'file'); @@ -376,7 +376,7 @@ suite('URI', () => { let uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008'); assert.equal(uri.query, 'LinkId=518008'); assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); + assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId=518008'); let uri2 = URI.parse(uri.toString()); assert.equal(uri2.query, 'LinkId=518008'); @@ -385,7 +385,7 @@ suite('URI', () => { uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); assert.equal(uri.query, 'LinkId=518008&foö&ké¥=üü'); assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); + assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId=518008&fo%C3%B6&k%C3%A9%C2%A5=%C3%BC%C3%BC'); uri2 = URI.parse(uri.toString()); assert.equal(uri2.query, 'LinkId=518008&foö&ké¥=üü'); @@ -426,6 +426,57 @@ suite('URI', () => { assert.equal(uri.toString(true), input); }); + test('Support URL specific encodings (query component) #25852', function () { + let input = 'http://example.com/over/there?name=ferret'; + assert.equal(input, URI.parse(input).toString()); + + input = 'http://example.com/over/there?name=ferret&foo=bar'; + assert.equal(input, URI.parse(input).toString()); + + input = 'attp://example.com/over/there?name=ferret'; + assert.equal('attp://example.com/over/there?name%3Dferret', URI.parse(input).toString()); + }); + + test('Uri#parse can break path-component #45515', function () { + let uri: URI; + uri = URI.from({ scheme: 's', authority: 'a', path: '/o%2f' }); + assert.equal(uri.toString(), 's://a/o%2f'); + uri = URI.from({ scheme: 's', authority: 'a', path: '/o%2fü' }); + assert.equal(uri.toString(), 's://a/o%2f%C3%BC'); + uri = URI.from({ scheme: 's', authority: 'a', path: '/o%2f%' }); + assert.equal(uri.toString(), 's://a/o%2f%25'); + + uri = URI.file('/test with %25/c#code'); + assert.equal(uri.path, '/test with %25/c#code'); + assert.equal(uri.toString(), 'file:///test%20with%20%25/c%23code'); + + uri = URI.from({ + scheme: 'http', + authority: 'a', + path: '/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg' + }); + assert.equal(uri.path, '/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg'); + assert.equal(uri.toString(), 'http://a/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg'); + + assert.equal(URI.parse(uri.toString()).path, '/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg'); + assert.equal(uri.toString(), URI.parse(uri.toString()).toString()); // identity + + uri = URI.parse('s://a/p%2ft%c3%bc'); + assert.equal(uri.path, '/p%2ftü'); + + uri = URI.parse('s://a/%c3%bcp%2f-REST'); + assert.equal(uri.path, '/üp%2f-REST'); + + uri = URI.parse('s://a/%c3%bcp%2fd%c3%b6wn'); + assert.equal(uri.path, '/üp%2fdöwn'); + + //https://github.com/microsoft/vscode/issues/25852 + uri = URI.parse('http://www.test.com/path/service?authId=CN%3DQ10'); + assert.equal(uri.query, 'authId=CN%3DQ10'); + assert.equal(uri.toString(), 'http://www.test.com/path/service?authId=CN%3DQ10'); + }); + + test('URI - (de)serialize', function () { const values = [ diff --git a/src/vs/code/code.main.ts b/src/vs/code/code.main.ts deleted file mode 100644 index 074dca196e1..00000000000 --- a/src/vs/code/code.main.ts +++ /dev/null @@ -1,6 +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 'vs/platform/update/node/update.config.contribution'; \ No newline at end of file diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index cee5824571f..240dd189202 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -9,7 +9,7 @@ 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 { WindowsService } from 'vs/platform/windows/electron-main/windowsService'; -import { ILifecycleService, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { ILifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/node/updateIpc'; @@ -36,12 +36,11 @@ import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import product from 'vs/platform/product/node/product'; import pkg from 'vs/platform/product/node/package'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; 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 { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard'; import { URI } from 'vs/base/common/uri'; import { WorkspacesChannel } from 'vs/platform/workspaces/node/workspacesIpc'; import { IWorkspacesMainService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; @@ -90,12 +89,7 @@ export class CodeApplication extends Disposable { private static APP_ICON_REFRESH_KEY = 'macOSAppIconRefresh7'; - private windowsMainService: IWindowsMainService; - - private electronIpcServer: ElectronIPCServer; - - private sharedProcess: SharedProcess; - private sharedProcessClient: Promise; + private windowsMainService: IWindowsMainService | undefined; constructor( private readonly mainIpcServer: Server, @@ -109,9 +103,6 @@ export class CodeApplication extends Disposable { ) { super(); - this._register(mainIpcServer); - this._register(configurationService); - this.registerListeners(); } @@ -122,12 +113,12 @@ export class CodeApplication extends Disposable { process.on('uncaughtException', err => this.onUnexpectedError(err)); process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason)); - // Contextmenu via IPC support - registerContextMenuListener(); - // Dispose on shutdown this.lifecycleService.onWillShutdown(() => this.dispose()); + // Contextmenu via IPC support + registerContextMenuListener(); + app.on('accessibility-support-changed', (event: Event, accessibilitySupportEnabled: boolean) => { if (this.windowsMainService) { this.windowsMainService.sendToAll('vscode:accessibilitySupportChanged', accessibilitySupportEnabled); @@ -200,7 +191,7 @@ export class CodeApplication extends Disposable { event.preventDefault(); // Keep in array because more might come! - macOpenFileURIs.push(getURIToOpenFromPathSync(path)); + macOpenFileURIs.push(this.getURIToOpenFromPathSync(path)); // Clear previous handler if any if (runningTimeout !== null) { @@ -217,6 +208,7 @@ export class CodeApplication extends Disposable { urisToOpen: macOpenFileURIs, preferNewWindow: true /* dropping on the dock or opening from finder prefers to open in a new window */ }); + macOpenFileURIs = []; runningTimeout = null; } @@ -224,7 +216,9 @@ export class CodeApplication extends Disposable { }); app.on('new-window-for-tab', () => { - this.windowsMainService.openNewWindow(OpenContext.DESKTOP); //macOS native tab "+" button + if (this.windowsMainService) { + this.windowsMainService.openNewWindow(OpenContext.DESKTOP); //macOS native tab "+" button + } }); ipc.on('vscode:exit', (event: Event, code: number) => { @@ -234,25 +228,26 @@ export class CodeApplication extends Disposable { this.lifecycleService.kill(code); }); - ipc.on('vscode:fetchShellEnv', (event: Event) => { + ipc.on('vscode:fetchShellEnv', async (event: Event) => { const webContents = event.sender; - getShellEnvironment(this.logService).then(shellEnv => { + + try { + const shellEnv = await getShellEnvironment(this.logService); if (!webContents.isDestroyed()) { webContents.send('vscode:acceptShellEnv', shellEnv); } - }, err => { + } catch (error) { if (!webContents.isDestroyed()) { webContents.send('vscode:acceptShellEnv', {}); } - this.logService.error('Error fetching shell env', err); - }); + this.logService.error('Error fetching shell env', error); + } }); ipc.on('vscode:extensionHostDebug', (_: Event, windowId: number, broadcast: any) => { if (this.windowsMainService) { - // Send to all windows (except sender window) - this.windowsMainService.sendToAll('vscode:extensionHostDebug', broadcast, [windowId]); + this.windowsMainService.sendToAll('vscode:extensionHostDebug', broadcast, [windowId]); // Send to all windows (except sender window) } }); @@ -261,11 +256,28 @@ export class CodeApplication extends Disposable { ipc.on('vscode:reloadWindow', (event: Event) => event.sender.reload()); - powerMonitor.on('resume', () => { // After waking up from sleep - if (this.windowsMainService) { - this.windowsMainService.sendToAll('vscode:osResume', undefined); - } - }); + // After waking up from sleep (after window opened) + (async () => { + await this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen); + + powerMonitor.on('resume', () => { + if (this.windowsMainService) { + this.windowsMainService.sendToAll('vscode:osResume', undefined); + } + }); + })(); + + // Keyboard layout changes (after window opened) + (async () => { + await this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen); + + const nativeKeymap = await import('native-keymap'); + nativeKeymap.onDidChangeKeyboardLayout(() => { + if (this.windowsMainService) { + this.windowsMainService.sendToAll('vscode:keyboardLayoutChanged', false); + } + }); + })(); } private onUnexpectedError(err: Error): void { @@ -317,24 +329,31 @@ export class CodeApplication extends Disposable { } // Create Electron IPC Server - this.electronIpcServer = new ElectronIPCServer(); + const electronIpcServer = new ElectronIPCServer(); // Resolve unique machine ID this.logService.trace('Resolving machine identifier...'); const machineId = await this.resolveMachineId(); this.logService.trace(`Resolved machine identifier: ${machineId}`); - // Spawn shared process - this.sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); - this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); + // Spawn shared process after the first window has opened and 3s have passed + const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); + const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); + this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { + this._register(new RunOnceScheduler(async () => { + const userEnv = await getShellEnvironment(this.logService); + + sharedProcess.spawn(userEnv); + }, 3000)).schedule(); + }); // Services - const appInstantiationService = await this.initServices(machineId); + const appInstantiationService = await this.createServices(machineId, sharedProcess, sharedProcessClient); // Create driver if (this.environmentService.driverHandle) { (async () => { - const server = await serveDriver(this.electronIpcServer, this.environmentService.driverHandle!, this.environmentService, appInstantiationService); + const server = await serveDriver(electronIpcServer, this.environmentService.driverHandle!, this.environmentService, appInstantiationService); this.logService.info('Driver started at:', this.environmentService.driverHandle); this._register(server); @@ -346,10 +365,10 @@ export class CodeApplication extends Disposable { this._register(authHandler); // Open Windows - const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor)); + const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)); // Post Open Windows Tasks - appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); + this.afterWindowOpen(); // Tracing: Stop tracing after windows are ready if enabled if (this.environmentService.args.trace) { @@ -371,6 +390,63 @@ export class CodeApplication extends Disposable { return machineId; } + private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessClient: Promise>): Promise { + const services = new ServiceCollection(); + + switch (process.platform) { + case 'win32': + services.set(IUpdateService, new SyncDescriptor(Win32UpdateService)); + break; + + case 'linux': + if (process.env.SNAP && process.env.SNAP_REVISION) { + services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env.SNAP, process.env.SNAP_REVISION])); + } else { + services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); + } + break; + + case 'darwin': + services.set(IUpdateService, new SyncDescriptor(DarwinUpdateService)); + break; + } + + services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, [machineId, this.userEnv])); + services.set(IWindowsService, new SyncDescriptor(WindowsService, [sharedProcess])); + services.set(ILaunchService, new SyncDescriptor(LaunchService)); + services.set(IIssueService, new SyncDescriptor(IssueService, [machineId, this.userEnv])); + services.set(IMenubarService, new SyncDescriptor(MenubarService)); + + const storageMainService = new StorageMainService(this.logService, this.environmentService); + services.set(IStorageMainService, storageMainService); + this.lifecycleService.onWillShutdown(e => e.join(storageMainService.close())); + + const backupMainService = new BackupMainService(this.environmentService, this.configurationService, this.logService); + services.set(IBackupMainService, backupMainService); + + services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService)); + services.set(IURLService, new SyncDescriptor(URLService)); + services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); + + // Telemetry + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + const channel = getDelayedChannel(sharedProcessClient.then(client => client.getChannel('telemetryAppender'))); + const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)); + const commonProperties = resolveCommonProperties(product.commit, pkg.version, machineId, this.environmentService.installSourcePath); + const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; + const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; + + services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config])); + } else { + services.set(ITelemetryService, NullTelemetryService); + } + + // Init services that require it + await backupMainService.initialize(); + + return this.instantiationService.createChild(services); + } + private stopTracingEventually(windows: ICodeWindow[]): void { this.logService.info(`Tracing: waiting for windows to get ready...`); @@ -384,12 +460,14 @@ export class CodeApplication extends Disposable { contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`), path => { if (!timeout) { - this.windowsMainService.showMessageBox({ - type: 'info', - message: localize('trace.message', "Successfully created trace."), - detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize('trace.ok', "Ok")] - }, this.windowsMainService.getLastActiveWindow()); + if (this.windowsMainService) { + this.windowsMainService.showMessageBox({ + type: 'info', + message: localize('trace.message', "Successfully created trace."), + detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), + buttons: [localize('trace.ok', "Ok")] + }, this.windowsMainService.getLastActiveWindow()); + } } else { this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`); } @@ -406,73 +484,7 @@ export class CodeApplication extends Disposable { }); } - private async initServices(machineId: string): Promise { - const services = new ServiceCollection(); - - if (process.platform === 'win32') { - services.set(IUpdateService, new SyncDescriptor(Win32UpdateService)); - } else if (process.platform === 'linux') { - if (process.env.SNAP && process.env.SNAP_REVISION) { - services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env.SNAP, process.env.SNAP_REVISION])); - } else { - services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); - } - } else if (process.platform === 'darwin') { - services.set(IUpdateService, new SyncDescriptor(DarwinUpdateService)); - } - - services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, [machineId])); - services.set(IWindowsService, new SyncDescriptor(WindowsService, [this.sharedProcess])); - services.set(ILaunchService, new SyncDescriptor(LaunchService)); - services.set(IIssueService, new SyncDescriptor(IssueService, [machineId, this.userEnv])); - services.set(IMenubarService, new SyncDescriptor(MenubarService)); - services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); - services.set(IBackupMainService, new SyncDescriptor(BackupMainService)); - services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService)); - services.set(IURLService, new SyncDescriptor(URLService)); - services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); - - // Telemetry - if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { - const channel = getDelayedChannel(this.sharedProcessClient.then(c => c.getChannel('telemetryAppender'))); - const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)); - const commonProperties = resolveCommonProperties(product.commit, pkg.version, machineId, this.environmentService.installSourcePath); - const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; - const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; - - services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config])); - } else { - services.set(ITelemetryService, NullTelemetryService); - } - - const appInstantiationService = this.instantiationService.createChild(services); - - // Init services that require it - await appInstantiationService.invokeFunction(accessor => Promise.all([ - this.initStorageService(accessor), - this.initBackupService(accessor) - ])); - - return appInstantiationService; - } - - private initStorageService(accessor: ServicesAccessor): Promise { - const storageMainService = accessor.get(IStorageMainService) as StorageMainService; - - // Ensure to close storage on shutdown - this.lifecycleService.onWillShutdown(e => e.join(storageMainService.close())); - - return Promise.resolve(); - } - - private initBackupService(accessor: ServicesAccessor): Promise { - const backupMainService = accessor.get(IBackupMainService) as BackupMainService; - - return backupMainService.initialize(); - } - - private openFirstWindow(accessor: ServicesAccessor): ICodeWindow[] { - const appInstantiationService = accessor.get(IInstantiationService); + private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] { // Register more Main IPC services const launchService = accessor.get(ILaunchService); @@ -482,40 +494,40 @@ export class CodeApplication extends Disposable { // Register more Electron IPC services const updateService = accessor.get(IUpdateService); const updateChannel = new UpdateChannel(updateService); - this.electronIpcServer.registerChannel('update', updateChannel); + electronIpcServer.registerChannel('update', updateChannel); const issueService = accessor.get(IIssueService); const issueChannel = new IssueChannel(issueService); - this.electronIpcServer.registerChannel('issue', issueChannel); + electronIpcServer.registerChannel('issue', issueChannel); const workspacesService = accessor.get(IWorkspacesMainService); - const workspacesChannel = appInstantiationService.createInstance(WorkspacesChannel, workspacesService); - this.electronIpcServer.registerChannel('workspaces', workspacesChannel); + const workspacesChannel = new WorkspacesChannel(workspacesService); + electronIpcServer.registerChannel('workspaces', workspacesChannel); const windowsService = accessor.get(IWindowsService); const windowsChannel = new WindowsChannel(windowsService); - this.electronIpcServer.registerChannel('windows', windowsChannel); - this.sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); + electronIpcServer.registerChannel('windows', windowsChannel); + sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); const menubarService = accessor.get(IMenubarService); const menubarChannel = new MenubarChannel(menubarService); - this.electronIpcServer.registerChannel('menubar', menubarChannel); + electronIpcServer.registerChannel('menubar', menubarChannel); const urlService = accessor.get(IURLService); const urlChannel = new URLServiceChannel(urlService); - this.electronIpcServer.registerChannel('url', urlChannel); + electronIpcServer.registerChannel('url', urlChannel); const storageMainService = accessor.get(IStorageMainService); const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService as StorageMainService)); - this.electronIpcServer.registerChannel('storage', storageChannel); + electronIpcServer.registerChannel('storage', storageChannel); // Log level management const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService)); - this.electronIpcServer.registerChannel('loglevel', logLevelChannel); - this.sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel)); + electronIpcServer.registerChannel('loglevel', logLevelChannel); + sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel)); - // Lifecycle - (this.lifecycleService as LifecycleService).ready(); + // Signal phase: ready (services set) + this.lifecycleService.phase = LifecycleMainPhase.Ready; // Propagate to clients const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService); // TODO@Joao: unfold this @@ -523,7 +535,7 @@ export class CodeApplication extends Disposable { // Create a URL handler which forwards to the last active window const activeWindowManager = new ActiveWindowManager(windowsService); const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); - const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', activeWindowRouter); + const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', activeWindowRouter); const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); // On Mac, Code can be running without any open windows, so we must create a window to handle urls, @@ -553,11 +565,9 @@ export class CodeApplication extends Disposable { // Watch Electron URLs and forward them to the UrlService const args = this.environmentService.args; const urls = args['open-url'] ? args._urls : []; - const urlListener = new ElectronURLListener(urls || [], urlService, this.windowsMainService); + const urlListener = new ElectronURLListener(urls || [], urlService, windowsMainService); this._register(urlListener); - this.windowsMainService.ready(this.userEnv); - // Open our first window const macOpenFiles: string[] = (global).macOpenFiles; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; @@ -567,9 +577,9 @@ export class CodeApplication extends Disposable { const noRecentEntry = args['skip-add-to-recently-opened'] === true; const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; + // new window if "-n" was used without paths if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { - // new window if "-n" was used without paths - return this.windowsMainService.open({ + return windowsMainService.open({ context, cli: args, forceNewWindow: true, @@ -582,10 +592,10 @@ export class CodeApplication extends Disposable { // mac: open-file event received on startup if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { - return this.windowsMainService.open({ + return windowsMainService.open({ context: OpenContext.DOCK, cli: args, - urisToOpen: macOpenFiles.map(getURIToOpenFromPathSync), + urisToOpen: macOpenFiles.map(file => this.getURIToOpenFromPathSync(file)), noRecentEntry, waitMarkerFileURI, initialStartup: true @@ -593,7 +603,7 @@ export class CodeApplication extends Disposable { } // default: read paths from cli - return this.windowsMainService.open({ + return windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), @@ -604,62 +614,31 @@ export class CodeApplication extends Disposable { }); } - private afterWindowOpen(accessor: ServicesAccessor): void { - const windowsMainService = accessor.get(IWindowsMainService); - const historyMainService = accessor.get(IHistoryMainService); - - if (isWindows) { - - // Setup Windows mutex - try { - const Mutex = (require.__$__nodeRequire('windows-mutex') as any).Mutex; - const windowsMutex = new Mutex(product.win32MutexName); - this._register(toDisposable(() => windowsMutex.release())); - } catch (e) { - if (!this.environmentService.isBuilt) { - windowsMainService.showMessageBox({ - title: product.nameLong, - type: 'warning', - message: 'Failed to load windows-mutex!', - detail: e.toString(), - noLink: true - }); - } + private getURIToOpenFromPathSync(path: string): IURIToOpen { + try { + const fileStat = statSync(path); + if (fileStat.isDirectory()) { + return { folderUri: URI.file(path) }; } - // Ensure Windows foreground love module - try { - // tslint:disable-next-line:no-unused-expression - require.__$__nodeRequire('windows-foreground-love'); - } catch (e) { - if (!this.environmentService.isBuilt) { - windowsMainService.showMessageBox({ - title: product.nameLong, - type: 'warning', - message: 'Failed to load windows-foreground-love!', - detail: e.toString(), - noLink: true - }); - } + if (hasWorkspaceFileExtension(path)) { + return { workspaceUri: URI.file(path) }; } + } catch (error) { + // ignore errors } + return { fileUri: URI.file(path) }; + } + + private afterWindowOpen(): void { + + // Signal phase: after window open + this.lifecycleService.phase = LifecycleMainPhase.AfterWindowOpen; + // Remote Authorities this.handleRemoteAuthorities(); - // Keyboard layout changes - KeyboardLayoutMonitor.INSTANCE.onDidChangeKeyboardLayout(() => { - this.windowsMainService.sendToAll('vscode:keyboardLayoutChanged', false); - }); - - // Jump List - historyMainService.updateWindowsJumpList(); - historyMainService.onRecentlyOpenedChange(() => historyMainService.updateWindowsJumpList()); - - // Start shared process after a while - const sharedProcessSpawn = this._register(new RunOnceScheduler(() => getShellEnvironment(this.logService).then(userEnv => this.sharedProcess.spawn(userEnv)), 3000)); - sharedProcessSpawn.schedule(); - // Helps application icon refresh after an update with new icon is installed (macOS) // TODO@Ben remove after a couple of releases if (isMacintosh) { @@ -697,8 +676,9 @@ export class CodeApplication extends Disposable { constructor(authority: string, host: string, port: number) { this._authority = authority; + const options: IConnectionOptions = { - isBuilt: isBuilt, + isBuilt, commit: product.commit, webSocketFactory: nodeWebSocketFactory, addressProvider: { @@ -707,6 +687,7 @@ export class CodeApplication extends Disposable { } } }; + this._connection = connectRemoteAgentManagement(options, authority, `main`); this._disposeRunner = new RunOnceScheduler(() => this.dispose(), 5000); } @@ -720,6 +701,7 @@ export class CodeApplication extends Disposable { async getClient(): Promise> { this._disposeRunner.schedule(); const connection = await this._connection; + return connection.client; } } @@ -727,7 +709,9 @@ export class CodeApplication extends Disposable { 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(); @@ -736,15 +720,19 @@ export class CodeApplication extends Disposable { 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 }; } }; @@ -753,6 +741,7 @@ export class CodeApplication extends Disposable { if (request.method !== 'GET') { return callback(undefined); } + const uri = URI.parse(request.url); let activeConnection: ActiveConnection | undefined; @@ -764,9 +753,11 @@ export class CodeApplication extends Disposable { callback(undefined); return; } + activeConnection = new ActiveConnection(uri.authority, resolvedAuthority.host, resolvedAuthority.port); connectionPool.set(uri.authority, activeConnection); } + try { const rawClient = await activeConnection!.getClient(); if (connectionPool.has(uri.authority)) { // not disposed in the meantime @@ -785,16 +776,3 @@ export class CodeApplication extends Disposable { }); } } - -function getURIToOpenFromPathSync(path: string): IURIToOpen { - try { - const fileStat = statSync(path); - if (fileStat.isDirectory()) { - return { folderUri: URI.file(path) }; - } else if (hasWorkspaceFileExtension(path)) { - return { workspaceUri: URI.file(path) }; - } - } catch (error) { - } - return { fileUri: URI.file(path) }; -} diff --git a/src/vs/code/electron-main/keyboard.ts b/src/vs/code/electron-main/keyboard.ts deleted file mode 100644 index 2ec1623f173..00000000000 --- a/src/vs/code/electron-main/keyboard.ts +++ /dev/null @@ -1,32 +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 nativeKeymap from 'native-keymap'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; - -export class KeyboardLayoutMonitor { - - public static readonly INSTANCE = new KeyboardLayoutMonitor(); - - private readonly _emitter: Emitter; - private _registered: boolean; - - private constructor() { - this._emitter = new Emitter(); - this._registered = false; - } - - public onDidChangeKeyboardLayout(callback: () => void): IDisposable { - if (!this._registered) { - this._registered = true; - - nativeKeymap.onDidChangeKeyboardLayout(() => { - this._emitter.fire(); - }); - } - return this._emitter.event(callback); - } -} \ No newline at end of file diff --git a/src/vs/code/electron-main/logUploader.ts b/src/vs/code/electron-main/logUploader.ts index 28ce4f7f1c1..4e601fe8a65 100644 --- a/src/vs/code/electron-main/logUploader.ts +++ b/src/vs/code/electron-main/logUploader.ts @@ -22,10 +22,10 @@ interface PostResult { class Endpoint { private constructor( - public readonly url: string + readonly url: string ) { } - public static getFromProduct(): Endpoint | undefined { + static getFromProduct(): Endpoint | undefined { const logUploaderUrl = product.logUploaderUrl; return logUploaderUrl ? new Endpoint(logUploaderUrl) : undefined; } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 0fb2ccfad31..12d0c606893 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/code/code.main'; +import 'vs/platform/update/node/update.config.contribution'; import { app, dialog } from 'electron'; import { assign } from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; @@ -39,6 +39,7 @@ import { uploadLogs } from 'vs/code/electron-main/logUploader'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { Client } from 'vs/base/parts/ipc/common/ipc.net'; +import { once } from 'vs/base/common/functional'; class ExpectedError extends Error { readonly isExpected = true; @@ -90,17 +91,14 @@ class CodeMain { // log file access on Windows (https://github.com/Microsoft/vscode/issues/41218) const bufferLogService = new BufferLogService(); - const instantiationService = this.createServices(args, bufferLogService); + const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService); try { + + // Init services await instantiationService.invokeFunction(async accessor => { const environmentService = accessor.get(IEnvironmentService); const stateService = accessor.get(IStateService); - const logService = accessor.get(ILogService); - // Patch `process.env` with the instance's environment - const instanceEnvironment = this.patchEnvironment(environmentService); - - // Startup try { await this.initServices(environmentService, stateService as StateService); } catch (error) { @@ -110,9 +108,19 @@ class CodeMain { throw error; } + }); + + // Startup + await instantiationService.invokeFunction(async accessor => { + const environmentService = accessor.get(IEnvironmentService); + const logService = accessor.get(ILogService); + const lifecycleService = accessor.get(ILifecycleService); + const configurationService = accessor.get(IConfigurationService); + + const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleService, instantiationService, true); - const mainIpcServer = await this.doStartup(logService, environmentService, instantiationService, true); bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel()); + once(lifecycleService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose()); return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); }); @@ -121,16 +129,17 @@ class CodeMain { } } - private createServices(args: ParsedArgs, bufferLogService: BufferLogService): IInstantiationService { + private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] { const services = new ServiceCollection(); const environmentService = new EnvironmentService(args, process.execPath); + const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment + services.set(IEnvironmentService, environmentService); const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]); process.once('exit', () => logService.dispose()); - - services.set(IEnvironmentService, environmentService); services.set(ILogService, logService); + services.set(ILifecycleService, new SyncDescriptor(LifecycleService)); services.set(IStateService, new SyncDescriptor(StateService)); services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.appSettingsPath])); @@ -138,12 +147,12 @@ class CodeMain { services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); - return new InstantiationService(services, true); + return [new InstantiationService(services, true), instanceEnvironment]; } private initServices(environmentService: IEnvironmentService, stateService: StateService): Promise { - // Ensure paths for environment service exist + // Environment service (paths) const environmentServiceInitialization = Promise.all([ environmentService.extensionsPath, environmentService.nodeCachedDataDir, @@ -175,7 +184,7 @@ class CodeMain { return instanceEnvironment; } - private async doStartup(logService: ILogService, environmentService: IEnvironmentService, instantiationService: IInstantiationService, retry: boolean): Promise { + private async doStartup(logService: ILogService, environmentService: IEnvironmentService, lifecycleService: ILifecycleService, instantiationService: IInstantiationService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means // we are the first instance to startup. Otherwise it is likely @@ -183,6 +192,7 @@ class CodeMain { let server: Server; try { server = await serve(environmentService.mainIPCHandle); + once(lifecycleService.onWillShutdown)(() => server.dispose()); } catch (error) { // Handle unexpected errors (the only expected error is EADDRINUSE that @@ -226,10 +236,11 @@ class CodeMain { fs.unlinkSync(environmentService.mainIPCHandle); } catch (error) { logService.warn('Could not delete obsolete instance handle', error); + throw error; } - return this.doStartup(logService, environmentService, instantiationService, false); + return this.doStartup(logService, environmentService, lifecycleService, instantiationService, false); } // Tests from CLI require to be the only instance currently @@ -261,8 +272,8 @@ class CodeMain { if (environmentService.args.status) { return instantiationService.invokeFunction(async accessor => { const diagnostics = await accessor.get(IDiagnosticsService).getDiagnostics(launchClient); - console.log(diagnostics); + throw new ExpectedError(); }); } @@ -300,12 +311,14 @@ class CodeMain { // Print --status usage info if (environmentService.args.status) { logService.warn('Warning: The --status argument can only be used if Code is already running. Please run it again after Code has started.'); + throw new ExpectedError('Terminating...'); } // Log uploader usage info if (typeof environmentService.args['upload-logs'] !== 'undefined') { logService.warn('Warning: The --upload-logs argument can only be used if Code is already running. Please run it again after Code has started.'); + throw new ExpectedError('Terminating...'); } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index e09a9a41dbd..b933aa0f33b 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -203,12 +203,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { return !!this.config.extensionTestsPath; } - /* - get extensionDevelopmentPaths(): string | string[] | undefined { - return this.config.extensionDevelopmentPath; - } - */ - get config(): IWindowConfiguration { return this.currentConfig; } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index b0213a2876e..b7446dc4074 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -15,7 +15,7 @@ import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; import { hasArgs, asArray } from 'vs/platform/environment/node/argv'; import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences, FileFilter } from 'electron'; import { parseLineAndColumnAware } from 'vs/code/node/paths'; -import { ILifecycleService, UnloadReason, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { ILifecycleService, UnloadReason, LifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult, INewWindowOptions, IURIToOpen, isFileToOpen, isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; @@ -38,6 +38,7 @@ import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage'; import { getWorkspaceIdentifier } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { once } from 'vs/base/common/functional'; const enum WindowError { UNRESPONSIVE = 1, @@ -159,9 +160,7 @@ export class WindowsManager implements IWindowsMainService { private static readonly windowsStateStorageKey = 'windowsState'; - private static WINDOWS: ICodeWindow[] = []; - - private initialUserEnv: IProcessEnvironment; + private static readonly WINDOWS: ICodeWindow[] = []; private readonly windowsState: IWindowsState; private lastClosedWindowState?: IWindowState; @@ -170,19 +169,20 @@ export class WindowsManager implements IWindowsMainService { private readonly workspacesManager: WorkspacesManager; private _onWindowReady = new Emitter(); - onWindowReady: CommonEvent = this._onWindowReady.event; + readonly onWindowReady: CommonEvent = this._onWindowReady.event; private _onWindowClose = new Emitter(); - onWindowClose: CommonEvent = this._onWindowClose.event; + readonly onWindowClose: CommonEvent = this._onWindowClose.event; private _onWindowLoad = new Emitter(); - onWindowLoad: CommonEvent = this._onWindowLoad.event; + readonly onWindowLoad: CommonEvent = this._onWindowLoad.event; private _onWindowsCountChanged = new Emitter(); - onWindowsCountChanged: CommonEvent = this._onWindowsCountChanged.event; + readonly onWindowsCountChanged: CommonEvent = this._onWindowsCountChanged.event; constructor( private readonly machineId: string, + private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @IStateService private readonly stateService: IStateService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @@ -203,12 +203,50 @@ export class WindowsManager implements IWindowsMainService { this.dialogs = new Dialogs(stateService, this); this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this); + + this.lifecycleService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); + this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.setupNativeHelpers()); } - ready(initialUserEnv: IProcessEnvironment): void { - this.initialUserEnv = initialUserEnv; + private setupNativeHelpers(): void { + if (isWindows) { - this.registerListeners(); + // Setup Windows mutex + try { + const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; + const mutex = new WindowsMutex(product.win32MutexName); + once(this.lifecycleService.onWillShutdown)(() => mutex.release()); + } catch (e) { + this.logService.error(e); + + if (!this.environmentService.isBuilt) { + this.showMessageBox({ + title: product.nameLong, + type: 'warning', + message: 'Failed to load windows-mutex!', + detail: e.toString(), + noLink: true + }); + } + } + + // Dev only: Ensure Windows foreground love module is present + if (!this.environmentService.isBuilt) { + try { + require.__$__nodeRequire('windows-foreground-love'); + } catch (e) { + this.logService.error(e); + + this.showMessageBox({ + title: product.nameLong, + type: 'warning', + message: 'Failed to load windows-foreground-love!', + detail: e.toString(), + noLink: true + }); + } + } + } } private registerListeners(): void { @@ -543,6 +581,7 @@ export class WindowsManager implements IWindowsMainService { // Find suitable window or folder path to open files in const fileToCheck = fileInputs.filesToOpenOrCreate[0] || fileInputs.filesToDiff[0]; + // only look at the windows with correct authority const windows = WindowsManager.WINDOWS.filter(w => w.remoteAuthority === fileInputs!.remoteAuthority); @@ -639,7 +678,6 @@ export class WindowsManager implements IWindowsMainService { // Handle folders to open (instructed and to restore) const allFoldersToOpen = arrays.distinct(foldersToOpen, folder => getComparisonKey(folder.folderUri)); // prevent duplicates - if (allFoldersToOpen.length > 0) { // Check for existing instances @@ -713,7 +751,9 @@ export class WindowsManager implements IWindowsMainService { if (fileInputs && !emptyToOpen) { emptyToOpen++; } + const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); + for (let i = 0; i < emptyToOpen; i++) { usedWindows.push(this.openInBrowserWindow({ userEnv: openConfig.userEnv, diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index d5f5e622c9f..e434b71a863 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -57,6 +57,7 @@ export async function main(argv: string[]): Promise { else if (shouldSpawnCliProcess(args)) { const cli = await new Promise((c, e) => require(['vs/code/node/cliProcessMain'], c, e)); await cli.main(args); + return; } diff --git a/src/vs/editor/contrib/codeAction/codeActionWidget.ts b/src/vs/editor/contrib/codeAction/codeActionWidget.ts index b3ddad603b0..480bb0ecdd7 100644 --- a/src/vs/editor/contrib/codeAction/codeActionWidget.ts +++ b/src/vs/editor/contrib/codeAction/codeActionWidget.ts @@ -27,12 +27,18 @@ export class CodeActionContextMenu { private readonly _onApplyCodeAction: (action: CodeAction) => Promise ) { } - async show(actionsToShow: Promise, at?: { x: number; y: number } | Position): Promise { + public async show(actionsToShow: Promise, at?: { x: number; y: number } | Position): Promise { const codeActions = await actionsToShow; + if (!codeActions.actions.length) { + this._visible = false; + return; + } if (!this._editor.getDomNode()) { // cancel when editor went off-dom + this._visible = false; return Promise.reject(canceled()); } + this._visible = true; const actions = codeActions.actions.map(action => this.codeActionToAction(action)); this._contextMenuService.showContextMenu({ diff --git a/src/vs/platform/configuration/test/common/configuration.test.ts b/src/vs/platform/configuration/test/common/configuration.test.ts index c8b29276730..17ee0b3e334 100644 --- a/src/vs/platform/configuration/test/common/configuration.test.ts +++ b/src/vs/platform/configuration/test/common/configuration.test.ts @@ -32,7 +32,7 @@ suite('Configuration', () => { assert.deepEqual(target, { 'a': { 'b': 2 } }); }); - test('removeFromValueTree: remove a single segemented key', () => { + test('removeFromValueTree: remove a single segmented key', () => { let target = { 'a': 1 }; removeFromValueTree(target, 'a'); @@ -40,7 +40,7 @@ suite('Configuration', () => { assert.deepEqual(target, {}); }); - test('removeFromValueTree: remove a single segemented key when its value is undefined', () => { + test('removeFromValueTree: remove a single segmented key when its value is undefined', () => { let target = { 'a': undefined }; removeFromValueTree(target, 'a'); @@ -48,7 +48,7 @@ suite('Configuration', () => { assert.deepEqual(target, {}); }); - test('removeFromValueTree: remove a multi segemented key when its value is undefined', () => { + test('removeFromValueTree: remove a multi segmented key when its value is undefined', () => { let target = { 'a': { 'b': 1 } }; removeFromValueTree(target, 'a.b'); @@ -56,7 +56,7 @@ suite('Configuration', () => { assert.deepEqual(target, {}); }); - test('removeFromValueTree: remove a multi segemented key when its value is array', () => { + test('removeFromValueTree: remove a multi segmented key when its value is array', () => { let target = { 'a': { 'b': [1] } }; removeFromValueTree(target, 'a.b'); @@ -64,7 +64,7 @@ suite('Configuration', () => { assert.deepEqual(target, {}); }); - test('removeFromValueTree: remove a multi segemented key first segment value is array', () => { + test('removeFromValueTree: remove a multi segmented key first segment value is array', () => { let target = { 'a': [1] }; removeFromValueTree(target, 'a.0'); @@ -80,7 +80,7 @@ suite('Configuration', () => { assert.deepEqual(target, {}); }); - test('removeFromValueTree: remove a multi segemented key when the first node has more values', () => { + test('removeFromValueTree: remove a multi segmented key when the first node has more values', () => { let target = { 'a': { 'b': { 'c': 1 }, 'd': 1 } }; removeFromValueTree(target, 'a.b.c'); @@ -88,7 +88,7 @@ suite('Configuration', () => { assert.deepEqual(target, { 'a': { 'd': 1 } }); }); - test('removeFromValueTree: remove a multi segemented key when in between node has more values', () => { + test('removeFromValueTree: remove a multi segmented key when in between node has more values', () => { let target = { 'a': { 'b': { 'c': { 'd': 1 }, 'd': 1 } } }; removeFromValueTree(target, 'a.b.c.d'); @@ -96,7 +96,7 @@ suite('Configuration', () => { assert.deepEqual(target, { 'a': { 'b': { 'd': 1 } } }); }); - test('removeFromValueTree: remove a multi segemented key when the last but one node has more values', () => { + test('removeFromValueTree: remove a multi segmented key when the last but one node has more values', () => { let target = { 'a': { 'b': { 'c': 1, 'd': 1 } } }; removeFromValueTree(target, 'a.b.c'); diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 2e7575c08a5..5e52e61ef90 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -82,7 +82,7 @@ suite('ConfigurationModel', () => { assert.deepEqual(testObject.keys, ['a.b']); }); - test('removeValue: remove a single segemented key', () => { + test('removeValue: remove a single segmented key', () => { let testObject = new ConfigurationModel({ 'a': 1 }, ['a']); testObject.removeValue('a'); @@ -91,7 +91,7 @@ suite('ConfigurationModel', () => { assert.deepEqual(testObject.keys, []); }); - test('removeValue: remove a multi segemented key', () => { + test('removeValue: remove a multi segmented key', () => { let testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']); testObject.removeValue('a.b'); diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index a7df288710d..f1b576222ec 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -36,7 +36,7 @@ export interface IConfirmationResult { /** * This will only be defined if the confirmation was created - * with the checkox option defined. + * with the checkbox option defined. */ checkboxChecked?: boolean; } diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 1e8288bbf1a..d3c0ab75d9e 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -287,6 +287,7 @@ export function parseDebugPort(debugArg: string | undefined, debugBrkArg: string const portStr = debugBrkArg || debugArg; const port = Number(portStr) || (!isBuild ? defaultBuildPort : null); const brk = port ? Boolean(!!debugBrkArg) : false; + return { port, break: brk, debugId }; } @@ -301,9 +302,9 @@ function parsePathArg(arg: string | undefined, process: NodeJS.Process): string if (path.normalize(arg) === resolved) { return resolved; - } else { - return path.resolve(process.env['VSCODE_CWD'] || process.cwd(), arg); } + + return path.resolve(process.env['VSCODE_CWD'] || process.cwd(), arg); } export function parseUserDataDir(args: ParsedArgs, process: NodeJS.Process): string { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index a89da0fa419..90d1c7349bd 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -480,12 +480,12 @@ export class ExtensionManagementService extends Disposable implements IExtension e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); } - private rename(identfier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { + private rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { return pfs.rename(extractPath, renamePath) .then(undefined, error => { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { - this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identfier.id); - return this.rename(identfier, extractPath, renamePath, retryUntil); + this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); + return this.rename(identifier, extractPath, renamePath, retryUntil); } return Promise.reject(new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING)); }); @@ -835,8 +835,8 @@ export class ExtensionManagementService extends Disposable implements IExtension return pfs.rimraf(extension.location.fsPath).then(() => this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath)); } - private isUninstalled(identfier: ExtensionIdentifierWithVersion): Promise { - return this.filterUninstalled(identfier).then(uninstalled => uninstalled.length === 1); + private isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise { + return this.filterUninstalled(identifier).then(uninstalled => uninstalled.length === 1); } private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts index 1cf171942b7..0e464b1db02 100644 --- a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts +++ b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts @@ -16,11 +16,11 @@ export class ExtensionsManifestCache extends Disposable { constructor( private readonly environmentService: IEnvironmentService, - extensionsManagementServuce: IExtensionManagementService + extensionsManagementService: IExtensionManagementService ) { super(); - this._register(extensionsManagementServuce.onDidInstallExtension(e => this.onDidInstallExtension(e))); - this._register(extensionsManagementServuce.onDidUninstallExtension(e => this.onDidUnInstallExtension(e))); + this._register(extensionsManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e))); + this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUnInstallExtension(e))); } private onDidInstallExtension(e: DidInstallExtensionEvent): void { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 8555f6f68f0..ac73dd3de19 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -517,7 +517,7 @@ interface IBaseStat { resource: URI; /** - * The name which is the last segement + * The name which is the last segment * of the {{path}}. */ name: string; @@ -531,7 +531,7 @@ interface IBaseStat { size?: number; /** - * The last modifictaion date represented + * The last modification date represented * as millis from unix epoch. * * The value may or may not be resolved as diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 4429cc10574..5116c3387d0 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -22,6 +22,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { getSimpleWorkspaceLabel } from 'vs/platform/label/common/label'; import { toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/history/electron-main/historyStorage'; import { exists } from 'vs/base/node/pfs'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { ILifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; export class HistoryMainService implements IHistoryMainService { @@ -37,10 +39,10 @@ export class HistoryMainService implements IHistoryMainService { private static readonly recentlyOpenedStorageKey = 'openedPathsList'; - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private _onRecentlyOpenedChange = new Emitter(); - onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; + readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; private macOSRecentDocumentsUpdater: ThrottledDelayer; @@ -48,9 +50,21 @@ export class HistoryMainService implements IHistoryMainService { @IStateService private readonly stateService: IStateService, @ILogService private readonly logService: ILogService, @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILifecycleService lifecycleService: ILifecycleService ) { this.macOSRecentDocumentsUpdater = new ThrottledDelayer(800); + + lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList()); + } + + private handleWindowsJumpList(): void { + if (!isWindows) { + return; // only on windows + } + + this.updateWindowsJumpList(); + this.onRecentlyOpenedChange(() => this.updateWindowsJumpList()); } addRecentlyOpened(newlyAdded: IRecent[]): void { diff --git a/src/vs/platform/launch/electron-main/launchService.ts b/src/vs/platform/launch/electron-main/launchService.ts index 091c8704a2c..d5d81a70bf8 100644 --- a/src/vs/platform/launch/electron-main/launchService.ts +++ b/src/vs/platform/launch/electron-main/launchService.ts @@ -8,7 +8,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURLService } from 'vs/platform/url/common/url'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { OpenContext, IWindowSettings } from 'vs/platform/windows/common/windows'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; @@ -107,7 +107,7 @@ export class LaunchChannel implements IServerChannel { export class LaunchChannelClient implements ILaunchService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; constructor(private channel: IChannel) { } @@ -134,7 +134,7 @@ export class LaunchChannelClient implements ILaunchService { export class LaunchService implements ILaunchService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; constructor( @ILogService private readonly logService: ILogService, diff --git a/src/vs/platform/lifecycle/common/lifecycle.ts b/src/vs/platform/lifecycle/common/lifecycle.ts index 18782a0d0b3..0cc2199b349 100644 --- a/src/vs/platform/lifecycle/common/lifecycle.ts +++ b/src/vs/platform/lifecycle/common/lifecycle.ts @@ -29,7 +29,7 @@ export interface BeforeShutdownEvent { /** * The reason why the application will be shutting down. */ - reason: ShutdownReason; + readonly reason: ShutdownReason; } /** @@ -51,7 +51,7 @@ export interface WillShutdownEvent { /** * The reason why the application is shutting down. */ - reason: ShutdownReason; + readonly reason: ShutdownReason; } export const enum ShutdownReason { diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index c218a17fa09..9a0702376bd 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -7,11 +7,12 @@ import { ipcMain as ipc, app } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/common/state'; import { Event, Emitter } from 'vs/base/common/event'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; +import { Barrier } from 'vs/base/common/async'; export const ILifecycleService = createDecorator('lifecycleService'); @@ -38,42 +39,48 @@ export interface ShutdownEvent { } export interface ILifecycleService { - _serviceBrand: any; + + _serviceBrand: ServiceIdentifier; /** * Will be true if the program was restarted (e.g. due to explicit request or update). */ - wasRestarted: boolean; + readonly wasRestarted: boolean; /** * Will be true if the program was requested to quit. */ - quitRequested: boolean; + readonly quitRequested: boolean; + + /** + * A flag indicating in what phase of the lifecycle we currently are. + */ + phase: LifecycleMainPhase; /** * An event that fires when the application is about to shutdown before any window is closed. * The shutdown can still be prevented by any window that vetos this event. */ - onBeforeShutdown: Event; + readonly onBeforeShutdown: Event; /** * An event that fires after the onBeforeShutdown event has been fired and after no window has * vetoed the shutdown sequence. At this point listeners are ensured that the application will * quit without veto. */ - onWillShutdown: Event; + readonly onWillShutdown: Event; /** * An event that fires before a window closes. This event is fired after any veto has been dealt * with so that listeners know for sure that the window will close without veto. */ - onBeforeWindowClose: Event; + readonly onBeforeWindowClose: Event; /** * An event that fires before a window is about to unload. Listeners can veto this event to prevent * the window from unloading. */ - onBeforeWindowUnload: Event; + readonly onBeforeWindowUnload: Event; /** * Unload a window for the provided reason. All lifecycle event handlers are triggered. @@ -94,6 +101,32 @@ export interface ILifecycleService { * Forcefully shutdown the application. No livecycle event handlers are triggered. */ kill(code?: number): void; + + /** + * Returns a promise that resolves when a certain lifecycle phase + * has started. + */ + when(phase: LifecycleMainPhase): Promise; +} + +export const enum LifecycleMainPhase { + + /** + * The first phase signals that we are about to startup. + */ + Starting = 1, + + /** + * Services are ready and first window is about to open. + */ + Ready = 2, + + /** + * This phase signals a point in time after the window has opened + * and is typically the best place to do work that is not required + * for the window to open. + */ + AfterWindowOpen = 3 } export class LifecycleService extends Disposable implements ILifecycleService { @@ -129,6 +162,11 @@ export class LifecycleService extends Disposable implements ILifecycleService { private readonly _onBeforeWindowUnload = this._register(new Emitter()); readonly onBeforeWindowUnload: Event = this._onBeforeWindowUnload.event; + private _phase: LifecycleMainPhase = LifecycleMainPhase.Starting; + get phase(): LifecycleMainPhase { return this._phase; } + + private phaseWhen = new Map(); + constructor( @ILogService private readonly logService: ILogService, @IStateService private readonly stateService: IStateService @@ -136,6 +174,7 @@ export class LifecycleService extends Disposable implements ILifecycleService { super(); this.handleRestarted(); + this.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); } private handleRestarted(): void { @@ -146,10 +185,6 @@ export class LifecycleService extends Disposable implements ILifecycleService { } } - ready(): void { - this.registerListeners(); - } - private registerListeners(): void { // before-quit: an event that is fired if application quit was @@ -238,6 +273,40 @@ export class LifecycleService extends Disposable implements ILifecycleService { return this.pendingWillShutdownPromise; } + set phase(value: LifecycleMainPhase) { + if (value < this.phase) { + throw new Error('Lifecycle cannot go backwards'); + } + + if (this._phase === value) { + return; + } + + this.logService.trace(`lifecycle (main): phase changed (value: ${value})`); + + this._phase = value; + + const barrier = this.phaseWhen.get(this._phase); + if (barrier) { + barrier.open(); + this.phaseWhen.delete(this._phase); + } + } + + async when(phase: LifecycleMainPhase): Promise { + if (phase <= this._phase) { + return; + } + + let barrier = this.phaseWhen.get(phase); + if (!barrier) { + barrier = new Barrier(); + this.phaseWhen.set(phase, barrier); + } + + await barrier.wait(); + } + registerWindow(window: ICodeWindow): void { // track window count @@ -390,10 +459,6 @@ export class LifecycleService extends Disposable implements ILifecycleService { }); } - /** - * A promise that completes to indicate if the quit request has been veto'd - * by the user or not. - */ quit(fromUpdate?: boolean): Promise { if (this.pendingQuitPromise) { return this.pendingQuitPromise; diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 257e1756aef..d0a48280bbe 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -27,22 +27,17 @@ export class FileStorage { } async init(): Promise { - try { - const contents = await readFile(this.dbPath); - - try { - this.lastFlushedSerializedDatabase = contents.toString(); - this._database = JSON.parse(this.lastFlushedSerializedDatabase); - } catch (error) { - this._database = {}; - } - } catch (error) { - if (error.code !== 'ENOENT') { - this.onError(error); - } - - this._database = {}; + if (this._database) { + return; // return if database was already loaded } + + const database = await this.loadAsync(); + + if (this._database) { + return; // return if database was already loaded + } + + this._database = database; } private loadSync(): object { @@ -59,6 +54,20 @@ export class FileStorage { } } + private async loadAsync(): Promise { + try { + this.lastFlushedSerializedDatabase = (await readFile(this.dbPath)).toString(); + + return JSON.parse(this.lastFlushedSerializedDatabase); + } catch (error) { + if (error.code !== 'ENOENT') { + this.onError(error); + } + + return {}; + } + } + getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; getItem(key: string, defaultValue?: T): T | undefined { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 2095d2e400b..cb72892f40e 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -92,7 +92,6 @@ export interface IWindowsMainService { readonly onWindowClose: Event; // methods - ready(initialUserEnv: IProcessEnvironment): void; reload(win: ICodeWindow, cli?: ParsedArgs): void; enterWorkspace(win: ICodeWindow, path: URI): Promise; closeWorkspace(win: ICodeWindow): void; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index de5a43cf497..6add914d3b9 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3515,6 +3515,10 @@ declare module 'vscode' { * * The editor will only resolve a completion item once. * + * *Note* that accepting a completion item will not wait for it to be resolved. Because of that [`insertText`](#CompletionItem.insertText), + * [`additionalTextEdits`](#CompletionItem.additionalTextEdits), and [`command`](#CompletionItem.command) should not + * be changed when resolving an item. + * * @param item A completion item currently active in the UI. * @param token A cancellation token. * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 9a6f866362c..aa0e454810a 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -469,6 +469,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _handlers = new Map(); private _commentControllers = new Map(); + private _activeCommentThread?: MainThreadCommentThread; + private _input?: modes.CommentInput; + private _openPanelListener: IDisposable | null; constructor( @@ -483,6 +486,26 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._disposables = []; this._activeCommentThreadDisposables = []; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); + + this._disposables.push(this._commentService.onDidChangeActiveCommentThread(async thread => { + let handle = (thread as MainThreadCommentThread).controllerHandle; + let controller = this._commentControllers.get(handle); + + if (!controller) { + return; + } + + this._activeCommentThreadDisposables = dispose(this._activeCommentThreadDisposables); + this._activeCommentThread = thread as MainThreadCommentThread; + controller.activeCommentThread = this._activeCommentThread; + + this._activeCommentThreadDisposables.push(this._activeCommentThread.onDidChangeInput(input => { // todo, dispose + this._input = input; + this._proxy.$onCommentWidgetInputChange(handle, URI.parse(this._activeCommentThread!.resource), this._activeCommentThread!.range, this._input ? this._input.value : undefined); + })); + + await this._proxy.$onCommentWidgetInputChange(controller.handle, URI.parse(this._activeCommentThread!.resource), this._activeCommentThread.range, this._input ? this._input.value : undefined); + })); } $registerCommentController(handle: number, id: string, label: string): void { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 0a33ff26d3b..5e79d7bed44 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -236,7 +236,7 @@ export function createApiFactory( }; // namespace: env - const env: typeof vscode.env = Object.freeze({ + const env: typeof vscode.env = { get machineId() { return initData.telemetryInfo.machineId; }, get sessionId() { return initData.telemetryInfo.sessionId; }, get language() { return initData.environment.appLanguage; }, @@ -257,7 +257,11 @@ export function createApiFactory( openExternal(uri: URI) { return extHostWindow.openUri(uri, { allowTunneling: !!initData.remoteAuthority }); } - }); + }; + if (!initData.environment.extensionTestsLocationURI) { + // allow to patch env-function when running tests + Object.freeze(env); + } // namespace: extensions const extensions: typeof vscode.extensions = { diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts index 3920c5d2d31..49385f06a2e 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -619,6 +619,7 @@ export class SimpleCommentService implements ICommentService { onDidSetAllCommentThreads: Event = Event.None; onDidUpdateCommentThreads: Event = Event.None; onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges; }> = Event.None; + onDidChangeActiveCommentThread: Event = Event.None; onDidSetDataProvider: Event = Event.None; onDidDeleteDataProvider: Event = Event.None; setDocumentComments: any; @@ -649,6 +650,7 @@ export class SimpleCommentService implements ICommentService { deleteReaction: any; getReactionGroup: any; toggleReaction: any; + setActiveCommentThread: any; } registerSingleton(ICommentService, SimpleCommentService, true); //#endregion diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 56c2a8667f9..09e2426a914 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -424,12 +424,14 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: this.comment.body.value }; + this.commentService.setActiveCommentThread(commentThread); this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => { commentThread.input = { uri: this._commentEditor!.getModel()!.uri, value: this.comment.body.value }; + this.commentService.setActiveCommentThread(commentThread); })); this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => { @@ -439,6 +441,7 @@ export class CommentNode extends Disposable { let input = commentThread.input; input.value = newVal; commentThread.input = input; + this.commentService.setActiveCommentThread(commentThread); } } })); @@ -486,6 +489,7 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: newBody }; + this.commentService.setActiveCommentThread(commentThread); let commandId = this.comment.editCommand.id; let args = this.comment.editCommand.arguments || []; @@ -523,6 +527,7 @@ export class CommentNode extends Disposable { if (result.confirmed) { try { if (this.comment.deleteCommand) { + this.commentService.setActiveCommentThread(this.commentThread); let commandId = this.comment.deleteCommand.id; let args = this.comment.deleteCommand.arguments || []; diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 4823e3aec3a..9b2286d4677 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -38,6 +38,7 @@ export interface ICommentService { readonly onDidSetResourceCommentInfos: Event; readonly onDidSetAllCommentThreads: Event; readonly onDidUpdateCommentThreads: Event; + readonly onDidChangeActiveCommentThread: Event; readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; @@ -69,6 +70,7 @@ export interface ICommentService { deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; getReactionGroup(owner: string): CommentReaction[] | undefined; toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise; + setActiveCommentThread(commentThread: CommentThread | null): void; } export class CommentService extends Disposable implements ICommentService { @@ -89,6 +91,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateCommentThreads: Event = this._onDidUpdateCommentThreads.event; + private readonly _onDidChangeActiveCommentThread = this._register(new Emitter()); + readonly onDidChangeActiveCommentThread = this._onDidChangeActiveCommentThread.event; + private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range, commentingRangesInfo: CommentingRanges @@ -109,6 +114,10 @@ export class CommentService extends Disposable implements ICommentService { super(); } + setActiveCommentThread(commentThread: CommentThread | null) { + this._onDidChangeActiveCommentThread.fire(commentThread); + } + setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 9d11f9114bf..737fe7297d4 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -211,6 +211,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._bodyElement = dom.$('.body'); container.appendChild(this._bodyElement); + + dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => { + this.commentService.setActiveCommentThread(this._commentThread); + }); } protected _fillHead(container: HTMLElement): void { @@ -265,6 +269,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { const deleteCommand = (this._commentThread as modes.CommentThread2).deleteCommand; if (deleteCommand) { + this.commentService.setActiveCommentThread(this._commentThread); return this.commandService.executeCommand(deleteCommand.id, ...(deleteCommand.arguments || [])); } else if (this._commentEditor.getValue() === '') { this.commentService.disposeCommentThread(this._owner, this._commentThread.threadId!); @@ -516,6 +521,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; + this.commentService.setActiveCommentThread(this._commentThread); })); this._commentThreadDisposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { @@ -526,6 +532,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget newInput.value = modelContent; thread.input = newInput; } + this.commentService.setActiveCommentThread(this._commentThread); })); this._commentThreadDisposables.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => { @@ -727,6 +734,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; + this.commentService.setActiveCommentThread(this._commentThread); await this.commandService.executeCommand(acceptInputCommand.id, ...(acceptInputCommand.arguments || [])); })); @@ -751,6 +759,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; + this.commentService.setActiveCommentThread(this._commentThread); await this.commandService.executeCommand(command.id, ...(command.arguments || [])); })); }); @@ -821,6 +830,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; + this.commentService.setActiveCommentThread(this._commentThread); let commandId = commentThread.acceptInputCommand.id; let args = commentThread.acceptInputCommand.arguments || []; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 1b15c1430cf..f198b06b03d 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -303,7 +303,7 @@ export class Scope extends ExpressionContainer implements IScope { indexedVariables?: number, public range?: IRange ) { - super(stackFrame.thread.session, reference, `scope:${stackFrame.getId()}:${name}:${index}`, namedVariables, indexedVariables); + super(stackFrame.thread.session, reference, `scope:${name}:${index}`, namedVariables, indexedVariables); } toString(): string { diff --git a/src/vs/workbench/contrib/debug/common/debugSource.ts b/src/vs/workbench/contrib/debug/common/debugSource.ts index 8079bda01de..133e3f8bec8 100644 --- a/src/vs/workbench/contrib/debug/common/debugSource.ts +++ b/src/vs/workbench/contrib/debug/common/debugSource.ts @@ -26,7 +26,6 @@ export const UNKNOWN_SOURCE_LABEL = nls.localize('unknownSource', "Unknown Sourc * | | | | * scheme source.path session id source.reference * - * the arbitrary_path and the session id are encoded with 'encodeURIComponent' * */ @@ -49,7 +48,11 @@ export class Source { } if (typeof this.raw.sourceReference === 'number' && this.raw.sourceReference > 0) { - this.uri = uri.parse(`${DEBUG_SCHEME}:${encodeURIComponent(path)}?session=${encodeURIComponent(sessionId)}&ref=${this.raw.sourceReference}`); + this.uri = uri.from({ + scheme: DEBUG_SCHEME, + path, + query: `session=${sessionId}&ref=${this.raw.sourceReference}` + }); } else { if (isUri(path)) { // path looks like a uri this.uri = uri.parse(path); @@ -60,7 +63,11 @@ export class Source { } else { // path is relative: since VS Code cannot deal with this by itself // create a debug url that will result in a DAP 'source' request when the url is resolved. - this.uri = uri.parse(`${DEBUG_SCHEME}:${encodeURIComponent(path)}?session=${encodeURIComponent(sessionId)}`); + this.uri = uri.from({ + scheme: DEBUG_SCHEME, + path, + query: `session=${sessionId}` + }); } } } @@ -118,7 +125,7 @@ export class Source { if (pair.length === 2) { switch (pair[0]) { case 'session': - sessionId = decodeURIComponent(pair[1]); + sessionId = pair[1]; break; case 'ref': sourceReference = parseInt(pair[1]); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index c4bcd08d1e8..53badb9e040 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -5,14 +5,11 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionHostProfile, ProfileSession, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { append, $, addDisposableListener } from 'vs/base/browser/dom'; -import { IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; -import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWindowsService } from 'vs/platform/windows/common/windows'; @@ -22,10 +19,11 @@ import product from 'vs/platform/product/node/product'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; export class ExtensionHostProfileService extends Disposable implements IExtensionHostProfileService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private readonly _onDidChangeState: Emitter = this._register(new Emitter()); public readonly onDidChangeState: Event = this._onDidChangeState.event; @@ -38,6 +36,9 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio private _profileSession: ProfileSession | null; private _state: ProfileSessionState; + private profilingStatusBarIndicator: IStatusbarEntryAccessor | undefined; + private profilingStatusBarIndicatorLabelUpdater: IDisposable | undefined; + public get state() { return this._state; } public get lastProfile() { return this._profile; } @@ -46,12 +47,18 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IWindowsService private readonly _windowsService: IWindowsService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, ) { super(); this._profile = null; this._profileSession = null; this._setState(ProfileSessionState.None); + + CommandsRegistry.registerCommand('workbench.action.extensionHostProfilder.stop', () => { + this.stopProfiling(); + this._editorService.openEditor(this._instantiationService.createInstance(RuntimeExtensionsInput), { revealIfOpened: true }); + }); } private _setState(state: ProfileSessionState): void { @@ -61,17 +68,48 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio this._state = state; if (this._state === ProfileSessionState.Running) { - ProfileExtHostStatusbarItem.instance.show(() => { - this.stopProfiling(); - this._editorService.openEditor(this._instantiationService.createInstance(RuntimeExtensionsInput), { revealIfOpened: true }); - }); + this.updateProfilingStatusBarIndicator(true); } else if (this._state === ProfileSessionState.Stopping) { - ProfileExtHostStatusbarItem.instance.hide(); + this.updateProfilingStatusBarIndicator(false); } this._onDidChangeState.fire(undefined); } + private updateProfilingStatusBarIndicator(visible: boolean): void { + if (this.profilingStatusBarIndicatorLabelUpdater) { + this.profilingStatusBarIndicatorLabelUpdater.dispose(); + this.profilingStatusBarIndicatorLabelUpdater = undefined; + } + + if (visible) { + const indicator: IStatusbarEntry = { + text: nls.localize('profilingExtensionHost', "$(sync~spin) Profiling Extension Host"), + tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."), + command: 'workbench.action.extensionHostProfilder.stop' + }; + + const timeStarted = Date.now(); + const handle = setInterval(() => { + if (this.profilingStatusBarIndicator) { + this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "$(sync~spin) Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); + } + }, 1000); + this.profilingStatusBarIndicatorLabelUpdater = toDisposable(() => clearInterval(handle)); + + if (!this.profilingStatusBarIndicator) { + this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, StatusbarAlignment.RIGHT); + } else { + this.profilingStatusBarIndicator.update(indicator); + } + } else { + if (this.profilingStatusBarIndicator) { + this.profilingStatusBarIndicator.dispose(); + this.profilingStatusBarIndicator = undefined; + } + } + } + public startProfiling(): Promise | null { if (this._state !== ProfileSessionState.None) { return null; @@ -134,76 +172,3 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio } } - -export class ProfileExtHostStatusbarItem implements IStatusbarItem { - - public static instance: ProfileExtHostStatusbarItem; - - private toDispose: IDisposable[]; - private statusBarItem: HTMLElement; - private label: HTMLElement; - private timeStarted: number; - private labelUpdater: any; - private clickHandler: (() => void) | null; - - constructor() { - ProfileExtHostStatusbarItem.instance = this; - this.toDispose = []; - this.timeStarted = 0; - } - - public show(clickHandler: () => void) { - this.clickHandler = clickHandler; - if (this.timeStarted === 0) { - this.timeStarted = new Date().getTime(); - this.statusBarItem.hidden = false; - this.labelUpdater = setInterval(() => { - this.updateLabel(); - }, 1000); - this.updateLabel(); - } - } - - public hide() { - this.clickHandler = null; - this.statusBarItem.hidden = true; - this.timeStarted = 0; - clearInterval(this.labelUpdater); - this.labelUpdater = null; - } - - public render(container: HTMLElement): IDisposable { - if (!this.statusBarItem && container) { - this.statusBarItem = append(container, $('.profileExtHost-statusbar-item')); - this.toDispose.push(addDisposableListener(this.statusBarItem, 'click', () => { - if (this.clickHandler) { - this.clickHandler(); - } - })); - this.statusBarItem.title = nls.localize('selectAndStartDebug', "Click to stop profiling."); - const a = append(this.statusBarItem, $('a')); - append(a, $('.icon')); - this.label = append(a, $('span.label')); - this.updateLabel(); - this.statusBarItem.hidden = true; - } - return this; - } - - private updateLabel() { - let label = 'Profiling Extension Host'; - if (this.timeStarted > 0) { - let secondsRecoreded = (new Date().getTime() - this.timeStarted) / 1000; - label = `Profiling Extension Host (${Math.round(secondsRecoreded)} sec)`; - } - this.label.textContent = label; - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } -} - -Registry.as(Extensions.Statusbar).registerStatusbarItem( - new StatusbarItemDescriptor(ProfileExtHostStatusbarItem, StatusbarAlignment.RIGHT) -); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts index ed90afcd03a..ea9a75df37f 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; import { SortBy, SortOrder, IQueryOptions, IExtensionTipsService, IExtensionRecommendation, IExtensionManagementServer, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -69,9 +69,13 @@ export interface ExtensionsListViewOptions extends IViewletViewOptions { server?: IExtensionManagementServer; } +class ExtensionListViewWarning extends Error { } + export class ExtensionsListView extends ViewletPanel { private readonly server: IExtensionManagementServer | undefined; + private messageContainer: HTMLElement; + private messageStatus: HTMLElement; private messageBox: HTMLElement; private extensionsList: HTMLElement; private badge: CountBadge; @@ -117,7 +121,9 @@ export class ExtensionsListView extends ViewletPanel { renderBody(container: HTMLElement): void { this.extensionsList = append(container, $('.extensions-list')); - this.messageBox = append(container, $('.message')); + this.messageContainer = append(container, $('.message-container')); + this.messageStatus = append(this.messageContainer, $('')); + this.messageBox = append(this.messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState); @@ -178,12 +184,11 @@ export class ExtensionsListView extends ViewletPanel { }; - const errorCallback = (e: Error) => { + const errorCallback = (e: any) => { const model = new PagedModel([]); if (!isPromiseCanceledError(e)) { this.queryRequest = null; - console.warn('Error querying extensions gallery', e); - this.setModel(model, true); + this.setModel(model, e); } return this.list ? this.list.model : model; }; @@ -238,7 +243,11 @@ export class ExtensionsListView extends ViewletPanel { if (ExtensionsListView.isLocalExtensionsQuery(query.value) || /@builtin/.test(query.value)) { return this.queryLocal(query, options); } - return this.queryGallery(query, options, token); + return this.queryGallery(query, options, token) + .then(null, e => { + console.warn('Error querying extensions gallery', getErrorMessage(e)); + return Promise.reject(new ExtensionListViewWarning(localize('galleryError', "We cannot connect to the Extensions Marketplace at this time, please try again later."))); + }); } private async queryByIds(ids: string[], options: IQueryOptions, token: CancellationToken): Promise> { @@ -696,23 +705,30 @@ export class ExtensionsListView extends ViewletPanel { }); } - private setModel(model: IPagedModel, isGalleryError?: boolean) { + private setModel(model: IPagedModel, error?: any) { if (this.list) { this.list.model = new DelayedPagedModel(model); this.list.scrollTop = 0; const count = this.count(); toggleClass(this.extensionsList, 'hidden', count === 0); - toggleClass(this.messageBox, 'hidden', count > 0); + toggleClass(this.messageContainer, 'hidden', count > 0); this.badge.setCount(count); if (count === 0 && this.isBodyVisible()) { - this.messageBox.textContent = isGalleryError ? localize('galleryError', "We cannot connect to the Extensions Marketplace at this time, please try again later.") : localize('no extensions found', "No extensions found."); - if (isGalleryError) { - alert(this.messageBox.textContent); + if (error) { + if (error instanceof ExtensionListViewWarning) { + this.messageStatus.className = 'message-status warning'; + this.messageBox.textContent = getErrorMessage(error); + } else { + this.messageStatus.className = 'message-status error'; + this.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error)); + } + } else { + this.messageStatus.className = ''; + this.messageBox.textContent = localize('no extensions found', "No extensions found."); } - } else { - this.messageBox.textContent = ''; + alert(this.messageBox.textContent); } } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css index ca9ddecdcbb..a70f0a7e411 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css @@ -38,7 +38,7 @@ } .extensions-viewlet > .extensions .extensions-list.hidden, -.extensions-viewlet > .extensions .message.hidden { +.extensions-viewlet > .extensions .message-container.hidden { display: none; visibility: hidden; } @@ -51,9 +51,35 @@ flex: 1; } -.extensions-viewlet > .extensions .message { +.extensions-viewlet > .extensions .message-container { padding: 5px 9px 5px 16px; cursor: default; + display: flex; +} + +.extensions-viewlet > .extensions .message-container .message-status { + height: 16px; + width: 16px; +} + +.extensions-viewlet > .extensions .message-container .message-status.warning { + background: url('status-warning.svg') center center no-repeat; +} + +.extensions-viewlet > .extensions .message-container .message-status.error { + background: url('status-error.svg') center center no-repeat; +} + +.vs-dark .extensions-viewlet > .extensions .message-container .message-status.warning { + background: url('status-warning-inverse.svg') center center no-repeat; +} + +.vs-dark .extensions-viewlet > .extensions .message-container .message-status.error { + background: url('status-error-inverse.svg') center center no-repeat; +} + +.extensions-viewlet > .extensions .message-container .message { + padding-left: 5px; } .extensions-viewlet > .extensions .monaco-list-row > .bookmark { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css b/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css index a63beff915d..cedd8541829 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css @@ -36,20 +36,3 @@ .runtime-extensions-editor .monaco-action-bar .actions-container { justify-content: left; } - -.monaco-workbench .part.statusbar .profileExtHost-statusbar-item .icon { - background: url('profile-stop.svg') no-repeat; - display: inline-block; - padding-right: 2px; - padding-bottom: 2px; - width: 16px; - height: 16px; - vertical-align: middle; - animation:fade 1000ms infinite; -} - -@keyframes fade { - from { opacity: 1.0; } - 50% { opacity: 0.5; } - to { opacity: 1.0; } -} diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/status-error-inverse.svg b/src/vs/workbench/contrib/extensions/electron-browser/media/status-error-inverse.svg new file mode 100644 index 00000000000..3c852a7ffde --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/status-error-inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/status-error.svg b/src/vs/workbench/contrib/extensions/electron-browser/media/status-error.svg new file mode 100644 index 00000000000..a1ddb39fed6 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/status-error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts index 117ce10374d..8fc47ec3f77 100644 --- a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts @@ -24,6 +24,7 @@ import { URI } from 'vs/base/common/uri'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowService } from 'vs/platform/windows/common/windows'; +import * as perf from 'vs/base/common/performance'; class PartsSplash { @@ -45,14 +46,20 @@ class PartsSplash { @IEditorGroupsService editorGroupsService: IEditorGroupsService, @IConfigurationService configService: IConfigurationService, ) { - lifecycleService.when(LifecyclePhase.Restored).then(_ => this._removePartsSplash()); + lifecycleService.when(LifecyclePhase.Restored).then(_ => { + this._removePartsSplash(); + perf.mark('didRemovePartsSplash'); + }); Event.debounce(Event.any( onDidChangeFullscreen, editorGroupsService.onDidLayout ), () => { }, 800)(this._savePartsSplash, this, this._disposables); configService.onDidChangeConfiguration(e => { - this._didChangeTitleBarStyle = e.affectsConfiguration('window.titleBarStyle'); + if (e.affectsConfiguration('window.titleBarStyle')) { + this._didChangeTitleBarStyle = true; + this._savePartsSplash(); + } }, this, this._disposables); } diff --git a/test/smoke/README.md b/test/smoke/README.md index 3eed271d355..0a6527ec0f6 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -15,6 +15,9 @@ yarn smoketest # Build yarn smoketest --build PATH_TO_NEW_BUILD_PARENT_FOLDER --stable-build PATH_TO_LAST_STABLE_BUILD_PARENT_FOLDER + +# Remote +yarn smoketest --build PATH_TO_NEW_BUILD_PARENT_FOLDER --remote ``` ### Run for a release diff --git a/test/smoke/src/areas/editor/peek.ts b/test/smoke/src/areas/editor/peek.ts index be1dcfe5b7e..dc25336625b 100644 --- a/test/smoke/src/areas/editor/peek.ts +++ b/test/smoke/src/areas/editor/peek.ts @@ -10,7 +10,7 @@ export class References { private static readonly REFERENCES_WIDGET = '.monaco-editor .zone-widget .zone-widget-container.peekview-widget.reference-zone-widget.results-loaded'; private static readonly REFERENCES_TITLE_FILE_NAME = `${References.REFERENCES_WIDGET} .head .peekview-title .filename`; private static readonly REFERENCES_TITLE_COUNT = `${References.REFERENCES_WIDGET} .head .peekview-title .meta`; - private static readonly REFERENCES = `${References.REFERENCES_WIDGET} .body .ref-tree.inline .monaco-list-row .reference`; + private static readonly REFERENCES = `${References.REFERENCES_WIDGET} .body .ref-tree.inline .monaco-list-row .highlight`; constructor(private code: Code) { } diff --git a/tslint.json b/tslint.json index 330af95aab9..53d4b924853 100644 --- a/tslint.json +++ b/tslint.json @@ -541,7 +541,6 @@ "**/vs/base/parts/**/{common,browser,node,electron-main}/**", "**/vs/platform/**/{common,browser,node,electron-main}/**", "**/vs/code/**/{common,browser,node,electron-main}/**", - "**/vs/code/code.main", "*" // node modules ] },