diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index f3a6482922e..7114f351353 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v30 + ref: v31 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 900f0793aca..7036a4937d4 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v30 + ref: v31 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/deep-classifier-monitor.yml b/.github/workflows/deep-classifier-monitor.yml index 8ecfc4c00b4..7aa6d9de22f 100644 --- a/.github/workflows/deep-classifier-monitor.yml +++ b/.github/workflows/deep-classifier-monitor.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: master + ref: v31 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index a2ac5ff6fb6..4ac237e5f83 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v30 + ref: v31 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index 5cbd74a9a19..c93f2b5352f 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: master + ref: v31 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index 7d6c292f98e..1ecc532ce88 100644 --- a/.github/workflows/english-please.yml +++ b/.github/workflows/english-please.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v30 + ref: v31 path: ./actions - name: Install Actions if: contains(github.event.issue.labels.*.name, '*english-please') diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index 884949d7ab5..893bba74a16 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -18,7 +18,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v30 + ref: v31 - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index 14e20555f67..4933b05fd0d 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -12,7 +12,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v30 + ref: v31 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 96ae94fe598..d805f6a6428 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v30 + ref: v31 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml index 4257764df86..3628c16ca83 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v30 + ref: v31 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Needs More Info Closer diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml index 39eca88b6de..97c8ee18e8e 100644 --- a/.github/workflows/on-label.yml +++ b/.github/workflows/on-label.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v30 + ref: v31 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 2d6eb4e0748..e8c41404d99 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v30 + ref: v31 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml index 13e06dde562..bc45221133f 100644 --- a/.github/workflows/release-pipeline-labeler.yml +++ b/.github/workflows/release-pipeline-labeler.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v30 + ref: v31 path: ./actions - name: Checkout Repo if: github.event_name != 'issues' diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 24275aa34ae..2dbf0e45871 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v30 + ref: v31 - name: Install Actions if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') run: npm install --production --prefix ./actions diff --git a/.vscode/settings.json b/.vscode/settings.json index 9239eae0d4d..b5251d42db0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,5 +76,6 @@ "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + "typescript.tsserver.useSeparateSyntaxServer": "dynamic" } diff --git a/build/.webignore b/build/.webignore new file mode 100644 index 00000000000..7d4503401f2 --- /dev/null +++ b/build/.webignore @@ -0,0 +1,15 @@ +# cleanup rules for web node modules, .gitignore style + +jschardet/src/** + +xterm/src/** + +xterm-addon-search/src/** +xterm-addon-search/out/** +xterm-addon-search/fixtures/** + +xterm-addon-unicode11/src/** +xterm-addon-unicode11/out/** + +xterm-addon-webgl/src/** +xterm-addon-webgl/out/** diff --git a/build/azure-pipelines/common/extract-telemetry.sh b/build/azure-pipelines/common/extract-telemetry.sh index 84bbd9c537c..6436e93c8c1 100755 --- a/build/azure-pipelines/common/extract-telemetry.sh +++ b/build/azure-pipelines/common/extract-telemetry.sh @@ -10,10 +10,10 @@ git clone --depth 1 https://github.com/Microsoft/vscode-node-debug2.git git clone --depth 1 https://github.com/Microsoft/vscode-node-debug.git git clone --depth 1 https://github.com/Microsoft/vscode-html-languageservice.git git clone --depth 1 https://github.com/Microsoft/vscode-json-languageservice.git -$BUILD_SOURCESDIRECTORY/build/node_modules/.bin/vscode-telemetry-extractor --sourceDir $BUILD_SOURCESDIRECTORY --excludedDir $BUILD_SOURCESDIRECTORY/extensions --outputDir . --applyEndpoints -$BUILD_SOURCESDIRECTORY/build/node_modules/.bin/vscode-telemetry-extractor --config $BUILD_SOURCESDIRECTORY/build/azure-pipelines/common/telemetry-config.json -o . +node $BUILD_SOURCESDIRECTORY/build/node_modules/.bin/vscode-telemetry-extractor --sourceDir $BUILD_SOURCESDIRECTORY --excludedDir $BUILD_SOURCESDIRECTORY/extensions --outputDir . --applyEndpoints +node $BUILD_SOURCESDIRECTORY/build/node_modules/.bin/vscode-telemetry-extractor --config $BUILD_SOURCESDIRECTORY/build/azure-pipelines/common/telemetry-config.json -o . mkdir -p $BUILD_SOURCESDIRECTORY/.build/telemetry mv declarations-resolved.json $BUILD_SOURCESDIRECTORY/.build/telemetry/telemetry-core.json mv config-resolved.json $BUILD_SOURCESDIRECTORY/.build/telemetry/telemetry-extensions.json cd .. -rm -rf extraction \ No newline at end of file +rm -rf extraction diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 466d3dd3328..fb4f3052578 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -218,7 +218,7 @@ steps: restoreSolution: 'build\azure-pipelines\win32\ESRPClient\packages.config' feedsToUse: config nugetConfigPath: 'build\azure-pipelines\win32\ESRPClient\NuGet.config' - externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b + externalFeedCredentials: 'ESRP Nuget' restoreDirectory: packages - task: ESRPImportCertTask@1 diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 5881bc66e91..d93496f928c 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -47,6 +47,7 @@ const nodeModules = ['electron', 'original-fs'] const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.desktop.main'), buildfile.base, + buildfile.workerExtensionHost, buildfile.workbenchDesktop, buildfile.code ]); @@ -72,6 +73,7 @@ const vscodeResources = [ 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', 'out-build/vs/workbench/contrib/webview/electron-browser/pre/*.js', + 'out-build/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/contrib/tasks/**/*.json', 'out-build/vs/platform/files/**/*.exe', diff --git a/build/lib/eslint/vscode-dts-literal-or-types.js b/build/lib/eslint/vscode-dts-literal-or-types.js index 02e6de876ba..e07dfc6de28 100644 --- a/build/lib/eslint/vscode-dts-literal-or-types.js +++ b/build/lib/eslint/vscode-dts-literal-or-types.js @@ -13,6 +13,10 @@ module.exports = new class ApiLiteralOrTypes { create(context) { return { ['TSTypeAnnotation TSUnionType TSLiteralType']: (node) => { + var _a; + if (((_a = node.literal) === null || _a === void 0 ? void 0 : _a.type) === 'TSNullKeyword') { + return; + } context.report({ node: node, messageId: 'useEnum' diff --git a/build/lib/eslint/vscode-dts-literal-or-types.ts b/build/lib/eslint/vscode-dts-literal-or-types.ts index 01a3eb21523..fe4befd84e7 100644 --- a/build/lib/eslint/vscode-dts-literal-or-types.ts +++ b/build/lib/eslint/vscode-dts-literal-or-types.ts @@ -15,6 +15,9 @@ export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { ['TSTypeAnnotation TSUnionType TSLiteralType']: (node: any) => { + if (node.literal?.type === 'TSNullKeyword') { + return; + } context.report({ node: node, messageId: 'useEnum' diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 170ea99bdd7..a2c2801a1dc 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.packageMarketplaceWebExtensionsStream = exports.packageMarketplaceExtensionsStream = exports.packageLocalWebExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; +exports.scanBuiltinExtensions = exports.packageMarketplaceWebExtensionsStream = exports.packageMarketplaceExtensionsStream = exports.packageLocalWebExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; const es = require("event-stream"); const fs = require("fs"); const glob = require("glob"); @@ -261,3 +261,31 @@ function packageMarketplaceWebExtensionsStream(builtInExtensions) { return es.merge(extensions); } exports.packageMarketplaceWebExtensionsStream = packageMarketplaceWebExtensionsStream; +function scanBuiltinExtensions(extensionsRoot, forWeb) { + const scannedExtensions = []; + const extensionsFolders = fs.readdirSync(extensionsRoot); + for (const extensionFolder of extensionsFolders) { + const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); + if (!fs.existsSync(packageJSONPath)) { + continue; + } + const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); + const extensionKind = packageJSON['extensionKind'] || []; + if (forWeb && extensionKind.indexOf('web') === -1) { + continue; + } + const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const packageNLS = children.filter(child => child === 'package.nls.json')[0]; + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; + scannedExtensions.push({ + extensionPath: extensionFolder, + packageJSON, + packageNLSPath: packageNLS ? path.join(extensionFolder, packageNLS) : undefined, + readmePath: readme ? path.join(extensionFolder, readme) : undefined, + changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, + }); + } + return scannedExtensions; +} +exports.scanBuiltinExtensions = scanBuiltinExtensions; diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index d0dfec9ff2a..7e2213dab46 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -78,7 +78,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { }); } - return minimizeLanguageJSON(input) + return minimizeLanguageJSON(input); } @@ -307,3 +307,39 @@ export function packageMarketplaceWebExtensionsStream(builtInExtensions: IBuiltI }); return es.merge(extensions); } + +export interface IScannedBuiltinExtension { + extensionPath: string, + packageJSON: any, + packageNLSPath?: string, + readmePath?: string, + changelogPath?: string, +} + +export function scanBuiltinExtensions(extensionsRoot: string, forWeb: boolean): IScannedBuiltinExtension[] { + const scannedExtensions: IScannedBuiltinExtension[] = []; + const extensionsFolders = fs.readdirSync(extensionsRoot); + for (const extensionFolder of extensionsFolders) { + const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); + if (!fs.existsSync(packageJSONPath)) { + continue; + } + const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); + const extensionKind: string[] = packageJSON['extensionKind'] || []; + if (forWeb && extensionKind.indexOf('web') === -1) { + continue; + } + const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const packageNLS = children.filter(child => child === 'package.nls.json')[0]; + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; + scannedExtensions.push({ + extensionPath: extensionFolder, + packageJSON, + packageNLSPath: packageNLS ? path.join(extensionFolder, packageNLS) : undefined, + readmePath: readme ? path.join(extensionFolder, readme) : undefined, + changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, + }); + } + return scannedExtensions; +} diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 93a6992e411..9bba404c243 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -218,6 +218,10 @@ "name": "vs/workbench/contrib/userDataSync", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/views", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/actions", "project": "vscode-workbench" diff --git a/build/package.json b/build/package.json index c56762aacad..e89ba75a597 100644 --- a/build/package.json +++ b/build/package.json @@ -44,9 +44,9 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.0.0-dev.20200615", + "typescript": "^4.0.0-dev.20200622", "vsce": "1.48.0", - "vscode-telemetry-extractor": "^1.5.4", + "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" }, "scripts": { diff --git a/build/win32/code.iss b/build/win32/code.iss index 74773730071..f1d4e8b747c 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -1132,7 +1132,7 @@ begin end; end; -// http://stackoverflow.com/a/23838239/261019 +// https://stackoverflow.com/a/23838239/261019 procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); var i, p: Integer; diff --git a/build/yarn.lock b/build/yarn.lock index 5c1e7b954ab..1686573f5e7 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -394,6 +394,11 @@ acorn@4.X: resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" @@ -845,7 +850,7 @@ debug@2.X, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^4.1.1: +debug@4, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -1415,6 +1420,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + iconv-lite-umd@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.3.tgz#61307cab8ac29939992d0724d3ab8799467f0e97" @@ -2051,6 +2064,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -2512,10 +2530,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.0.0-dev.20200615: - version "4.0.0-dev.20200615" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200615.tgz#5c06a0d5f25a29a018767970c6531fbbed7240e3" - integrity sha512-OD7KRTLimUwW5E1xHsAqXNjw0O0Krk9CgRVFYkqANv4fZisaN1LJI06u30D5QiNnHBzm2nBSzZIAhjj4MUqaRA== +typescript@^4.0.0-dev.20200622: + version "4.0.0-dev.20200622" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200622.tgz#33e0ffaf880b1f16bde5bc4eeb1863e52c4d7f75" + integrity sha512-KWXppG2OKfq5cDAEkc0wA7uemXnF/Af4v0j08plUCKk20rt9wYU2rU9EB53/XlVeZgV2hwpbH9hIFyeB4dWvdg== typical@^4.0.0: version "4.0.0" @@ -2647,19 +2665,22 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-ripgrep@^1.5.6: - version "1.5.7" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.5.7.tgz#acb6b548af488a4bca5d0f1bb5faf761343289ce" - integrity sha512-/Vsz/+k8kTvui0q3O74pif9FK0nKopgFTiGNVvxicZANxtSA8J8gUE9GQ/4dpi7D/2yI/YVORszwVskFbz46hQ== +vscode-ripgrep@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.6.2.tgz#fb912c7465699f10ce0218a6676cc632c77369b4" + integrity sha512-jkZEWnQFcE+QuQFfxQXWcWtDafTmgkp3DjMKawDkajZwgnDlGKpFp15ybKrZNVTi1SLEF/12BzxYSZVVZ2XrkA== + dependencies: + https-proxy-agent "^4.0.0" + proxy-from-env "^1.1.0" -vscode-telemetry-extractor@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.5.4.tgz#bcb0d17667fa1b77715e3a3bf372ade18f846782" - integrity sha512-MN9LNPo0Rc6cy3sIWTAG97PTWkEKdRnP0VeYoS8vjKSNtG9CAsrUxHgFfYoHm2vNK/ijd0a4NzETyVGO2kT6hw== +vscode-telemetry-extractor@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.6.0.tgz#e9d9c1d24863cce8d3d715f0287de3b31eb90c56" + integrity sha512-zSxvkbyAMa1lTRGIHfGg7gW2e9Sey+2zGYD19uNWCsVEfoXAr2NB6uzb0sNHtbZ2SSqxSePmFXzBAavsudT5fw== dependencies: command-line-args "^5.1.1" ts-morph "^3.1.3" - vscode-ripgrep "^1.5.6" + vscode-ripgrep "^1.6.2" vso-node-api@6.1.2-preview: version "6.1.2-preview" diff --git a/extensions/javascript/snippets/javascript.code-snippets b/extensions/javascript/snippets/javascript.code-snippets index 5da4ebe0c18..fc892b57e92 100644 --- a/extensions/javascript/snippets/javascript.code-snippets +++ b/extensions/javascript/snippets/javascript.code-snippets @@ -173,8 +173,7 @@ "Log to the console": { "prefix": "log", "body": [ - "console.log($1);", - "$0" + "console.log($1);" ], "description": "Log to the console" }, @@ -182,7 +181,6 @@ "prefix": "warn", "body": [ "console.warn($1);", - "$0" ], "description": "Log warning to the console" }, @@ -190,7 +188,6 @@ "prefix": "error", "body": [ "console.error($1);", - "$0" ], "description": "Log error to the console" } diff --git a/extensions/typescript-language-features/src/features/callHierarchy.ts b/extensions/typescript-language-features/src/features/callHierarchy.ts index a76d8a6bea8..3e84d255ee5 100644 --- a/extensions/typescript-language-features/src/features/callHierarchy.ts +++ b/extensions/typescript-language-features/src/features/callHierarchy.ts @@ -11,6 +11,13 @@ import { VersionDependentRegistration } from '../utils/dependentRegistration'; import type * as Proto from '../protocol'; import * as path from 'path'; import * as PConst from '../protocol.const'; +import { parseKindModifier } from '../utils/modifiers'; + +namespace Experimental { + export interface CallHierarchyItem extends Proto.CallHierarchyItem { + readonly kindModifiers?: string; + } +} class TypeScriptCallHierarchySupport implements vscode.CallHierarchyProvider { public static readonly minVersion = API.v380; @@ -75,11 +82,11 @@ function isSourceFileItem(item: Proto.CallHierarchyItem) { return item.kind === PConst.Kind.script || item.kind === PConst.Kind.module && item.selectionSpan.start.line === 1 && item.selectionSpan.start.offset === 1; } -function fromProtocolCallHierarchyItem(item: Proto.CallHierarchyItem): vscode.CallHierarchyItem { +function fromProtocolCallHierarchyItem(item: Experimental.CallHierarchyItem): vscode.CallHierarchyItem { const useFileName = isSourceFileItem(item); const name = useFileName ? path.basename(item.file) : item.name; const detail = useFileName ? vscode.workspace.asRelativePath(path.dirname(item.file)) : ''; - return new vscode.CallHierarchyItem( + const result = new vscode.CallHierarchyItem( typeConverters.SymbolKind.fromProtocolScriptElementKind(item.kind), name, detail, @@ -87,6 +94,12 @@ function fromProtocolCallHierarchyItem(item: Proto.CallHierarchyItem): vscode.Ca typeConverters.Range.fromTextSpan(item.span), typeConverters.Range.fromTextSpan(item.selectionSpan) ); + + const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined; + if (kindModifiers?.has(PConst.KindModifiers.depreacted)) { + result.tags = [vscode.SymbolTag.Deprecated]; + } + return result; } function fromProtocolCallHierchyIncomingCall(item: Proto.CallHierarchyIncomingCall): vscode.CallHierarchyIncomingCall { diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index 97051395de9..79f617ba4d7 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -19,6 +19,7 @@ import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; import TypingsStatus from '../utils/typingsStatus'; import FileConfigurationManager from './fileConfigurationManager'; +import { parseKindModifier } from '../utils/modifiers'; const localize = nls.loadMessageBundle(); @@ -90,8 +91,8 @@ class MyCompletionItem extends vscode.CompletionItem { } if (tsEntry.kindModifiers) { - const kindModifiers = tsEntry.kindModifiers.split(/,|\s+/g); - if (kindModifiers.includes(PConst.KindModifiers.optional)) { + const kindModifiers = parseKindModifier(tsEntry.kindModifiers); + if (kindModifiers.has(PConst.KindModifiers.optional)) { if (!this.insertText) { this.insertText = this.label; } @@ -101,14 +102,17 @@ class MyCompletionItem extends vscode.CompletionItem { } this.label += '?'; } + if (kindModifiers.has(PConst.KindModifiers.depreacted)) { + this.tags = [vscode.CompletionItemTag.Deprecated]; + } - if (kindModifiers.includes(PConst.KindModifiers.color)) { + if (kindModifiers.has(PConst.KindModifiers.color)) { this.kind = vscode.CompletionItemKind.Color; } if (tsEntry.kind === PConst.Kind.script) { for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) { - if (kindModifiers.includes(extModifier)) { + if (kindModifiers.has(extModifier)) { if (tsEntry.name.toLowerCase().endsWith(extModifier)) { this.detail = tsEntry.name; } else { diff --git a/extensions/typescript-language-features/src/features/diagnostics.ts b/extensions/typescript-language-features/src/features/diagnostics.ts index 327573e8c18..ec0566d67cb 100644 --- a/extensions/typescript-language-features/src/features/diagnostics.ts +++ b/extensions/typescript-language-features/src/features/diagnostics.ts @@ -78,7 +78,7 @@ class FileDiagnostics { return this.get(DiagnosticKind.Suggestion).filter(x => { if (!enableSuggestions) { // Still show unused - return x.tags && x.tags.includes(vscode.DiagnosticTag.Unnecessary); + return x.tags && (x.tags.includes(vscode.DiagnosticTag.Unnecessary) || x.tags.includes(vscode.DiagnosticTag.Deprecated)); } return true; }); diff --git a/extensions/typescript-language-features/src/features/documentSymbol.ts b/extensions/typescript-language-features/src/features/documentSymbol.ts index e119b005bab..8b44581c6de 100644 --- a/extensions/typescript-language-features/src/features/documentSymbol.ts +++ b/extensions/typescript-language-features/src/features/documentSymbol.ts @@ -9,6 +9,7 @@ import * as PConst from '../protocol.const'; import { ITypeScriptServiceClient } from '../typescriptService'; import * as typeConverters from '../utils/typeConverters'; import { CachedResponse } from '../tsServer/cachedResponse'; +import { parseKindModifier } from '../utils/modifiers'; const getSymbolKind = (kind: string): vscode.SymbolKind => { switch (kind) { @@ -79,6 +80,12 @@ class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider range, range.contains(selectionRange) ? selectionRange : range); + + const kindModifiers = parseKindModifier(item.kindModifiers); + if (kindModifiers.has(PConst.KindModifiers.depreacted)) { + symbolInfo.tags = [vscode.SymbolTag.Deprecated]; + } + for (const child of children) { if (child.spans.some(span => !!range.intersection(typeConverters.Range.fromTextSpan(span)))) { const includedChild = TypeScriptDocumentSymbolProvider.convertNavTree(resource, symbolInfo.children, child); diff --git a/extensions/typescript-language-features/src/features/workspaceSymbols.ts b/extensions/typescript-language-features/src/features/workspaceSymbols.ts index be269a8ec41..73a142440e5 100644 --- a/extensions/typescript-language-features/src/features/workspaceSymbols.ts +++ b/extensions/typescript-language-features/src/features/workspaceSymbols.ts @@ -11,6 +11,7 @@ import API from '../utils/api'; import * as fileSchemes from '../utils/fileSchemes'; import { doesResourceLookLikeAJavaScriptFile, doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription'; import * as typeConverters from '../utils/typeConverters'; +import { parseKindModifier } from '../utils/modifiers'; function getSymbolKind(item: Proto.NavtoItem): vscode.SymbolKind { switch (item.kind) { @@ -90,11 +91,16 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide private toSymbolInformation(item: Proto.NavtoItem) { const label = TypeScriptWorkspaceSymbolProvider.getLabel(item); - return new vscode.SymbolInformation( + const info = new vscode.SymbolInformation( label, getSymbolKind(item), item.containerName || '', typeConverters.Location.fromTextSpan(this.client.toResource(item.file), item)); + const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined; + if (kindModifiers?.has(PConst.KindModifiers.depreacted)) { + info.tags = [vscode.SymbolTag.Deprecated]; + } + return info; } private static getLabel(item: Proto.NavtoItem) { diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 644eed40441..ee040667b68 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -122,16 +122,22 @@ export default class LanguageProvider extends Disposable { this.client.bufferSyncSupport.requestAllDiagnostics(); } - public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any })[]): void { + public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[]): void { const config = vscode.workspace.getConfiguration(this.id, file); const reportUnnecessary = config.get('showUnused', true); + const reportDeprecated = config.get('showDeprecated', true); this.client.diagnosticsManager.updateDiagnostics(file, this._diagnosticLanguage, diagnosticsKind, diagnostics.filter(diag => { + // Don't both reporting diagnostics we know will not be rendered if (!reportUnnecessary) { - diag.tags = undefined; if (diag.reportUnnecessary && diag.severity === vscode.DiagnosticSeverity.Hint) { return false; } } + if (!reportDeprecated) { + if (diag.reportDeprecated && diag.severity === vscode.DiagnosticSeverity.Hint) { + return false; + } + } return true; })); } diff --git a/extensions/typescript-language-features/src/protocol.const.ts b/extensions/typescript-language-features/src/protocol.const.ts index ff2bfece14f..210e962c9aa 100644 --- a/extensions/typescript-language-features/src/protocol.const.ts +++ b/extensions/typescript-language-features/src/protocol.const.ts @@ -45,6 +45,7 @@ export class DiagnosticCategory { export class KindModifiers { public static readonly optional = 'optional'; + public static readonly depreacted = 'deprecated'; public static readonly color = 'color'; public static readonly dtsFile = '.d.ts'; diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 32f0e47609a..1f1beaa382a 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -26,6 +26,12 @@ import * as typeConverters from './utils/typeConverters'; import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; import VersionStatus from './utils/versionStatus'; +namespace Experimental { + export interface Diagnostic extends Proto.Diagnostic { + readonly reportsDeprecated?: {} + } +} + // Style check diagnostics that can be reported as warnings const styleCheckDiagnostics = new Set([ ...errorCodes.variableDeclaredButNeverUsed, @@ -233,11 +239,11 @@ export default class TypeScriptServiceClientHost extends Disposable { private createMarkerDatas( diagnostics: Proto.Diagnostic[], source: string - ): (vscode.Diagnostic & { reportUnnecessary: any })[] { + ): (vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[] { return diagnostics.map(tsDiag => this.tsDiagnosticToVsDiagnostic(tsDiag, source)); } - private tsDiagnosticToVsDiagnostic(diagnostic: Proto.Diagnostic, source: string): vscode.Diagnostic & { reportUnnecessary: any } { + private tsDiagnosticToVsDiagnostic(diagnostic: Experimental.Diagnostic, source: string): vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any } { const { start, end, text } = diagnostic; const range = new vscode.Range(typeConverters.Position.fromLocation(start), typeConverters.Position.fromLocation(end)); const converted = new vscode.Diagnostic(range, text, this.getDiagnosticSeverity(diagnostic)); @@ -255,11 +261,19 @@ export default class TypeScriptServiceClientHost extends Disposable { return new vscode.DiagnosticRelatedInformation(typeConverters.Location.fromTextSpan(this.client.toResource(span.file), span), info.message); })); } + const tags: vscode.DiagnosticTag[] = []; if (diagnostic.reportsUnnecessary) { - converted.tags = [vscode.DiagnosticTag.Unnecessary]; + tags.push(vscode.DiagnosticTag.Unnecessary); } - (converted as vscode.Diagnostic & { reportUnnecessary: any }).reportUnnecessary = diagnostic.reportsUnnecessary; - return converted as vscode.Diagnostic & { reportUnnecessary: any }; + if (diagnostic.reportsDeprecated) { + tags.push(vscode.DiagnosticTag.Deprecated); + } + converted.tags = tags.length ? tags : undefined; + + const resultConverted = converted as vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any }; + resultConverted.reportUnnecessary = diagnostic.reportsUnnecessary; + resultConverted.reportDeprecated = diagnostic.reportsDeprecated; + return resultConverted; } private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): vscode.DiagnosticSeverity { diff --git a/extensions/typescript-language-features/src/utils/modifiers.ts b/extensions/typescript-language-features/src/utils/modifiers.ts new file mode 100644 index 00000000000..589b5da3d52 --- /dev/null +++ b/extensions/typescript-language-features/src/utils/modifiers.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function parseKindModifier(kindModifiers: string): Set { + return new Set(kindModifiers.split(/,|\s+/g)); +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index d1d6f3e7fb4..ecc319f4106 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind } from 'vscode'; +import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, QuickPickItem, TextEditor } from 'vscode'; import { join } from 'path'; import { closeAllEditors, pathEquals, createRandomFile } from '../utils'; + suite('vscode API - window', () => { teardown(closeAllEditors); @@ -146,6 +147,20 @@ suite('vscode API - window', () => { }); test('active editor not always correct... #49125', async function () { + + function assertActiveEditor(editor: TextEditor) { + if (window.activeTextEditor === editor) { + assert.ok(true); + return; + } + function printEditor(editor: TextEditor): string { + return `doc: ${editor.document.uri.toString()}, column: ${editor.viewColumn}, active: ${editor === window.activeTextEditor}`; + } + const visible = window.visibleTextEditors.map(editor => printEditor(editor)); + assert.ok(false, `ACTIVE editor should be ${printEditor(editor)}, BUT HAVING ${visible.join(', ')}`); + + } + const randomFile1 = await createRandomFile(); const randomFile2 = await createRandomFile(); @@ -155,10 +170,10 @@ suite('vscode API - window', () => { ]); for (let c = 0; c < 4; c++) { let editorA = await window.showTextDocument(docA, ViewColumn.One); - assert.equal(window.activeTextEditor, editorA); + assertActiveEditor(editorA); let editorB = await window.showTextDocument(docB, ViewColumn.Two); - assert.equal(window.activeTextEditor, editorB); + assertActiveEditor(editorB); } }); @@ -383,33 +398,31 @@ suite('vscode API - window', () => { assert.equal(await two, 'notempty'); }); - // TODO@chrmarti Disabled due to flaky behaviour (https://github.com/Microsoft/vscode/issues/70887) - // test('showQuickPick, accept first', async function () { - // const pick = window.showQuickPick(['eins', 'zwei', 'drei']); - // await new Promise(resolve => setTimeout(resolve, 10)); // Allow UI to update. - // await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - // assert.equal(await pick, 'eins'); - // }); - - test('showQuickPick, accept second', async function () { - const resolves: ((value: string) => void)[] = []; - let done: () => void; - const unexpected = new Promise((resolve, reject) => { - done = () => resolve(); - resolves.push(reject); - }); - const first = new Promise(resolve => resolves.push(resolve)); + test('showQuickPick, accept first', async function () { + const tracker = createQuickPickTracker(); + const first = tracker.nextItem(); const pick = window.showQuickPick(['eins', 'zwei', 'drei'], { - onDidSelectItem: item => resolves.pop()!(item as string) + onDidSelectItem: tracker.onDidSelectItem }); assert.equal(await first, 'eins'); - const second = new Promise(resolve => resolves.push(resolve)); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + assert.equal(await pick, 'eins'); + return tracker.done(); + }); + + test('showQuickPick, accept second', async function () { + const tracker = createQuickPickTracker(); + const first = tracker.nextItem(); + const pick = window.showQuickPick(['eins', 'zwei', 'drei'], { + onDidSelectItem: tracker.onDidSelectItem + }); + assert.equal(await first, 'eins'); + const second = tracker.nextItem(); await commands.executeCommand('workbench.action.quickOpenSelectNext'); assert.equal(await second, 'zwei'); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); assert.equal(await pick, 'zwei'); - done!(); - return unexpected; + return tracker.done(); }); test('showQuickPick, select first two', async function () { @@ -438,19 +451,27 @@ suite('vscode API - window', () => { return unexpected; }); - // TODO@chrmarti Disabled due to flaky behaviour (https://github.com/Microsoft/vscode/issues/70887) - // test('showQuickPick, keep selection (Microsoft/vscode-azure-account#67)', async function () { - // const picks = window.showQuickPick([ - // { label: 'eins' }, - // { label: 'zwei', picked: true }, - // { label: 'drei', picked: true } - // ], { - // canPickMany: true - // }); - // await new Promise(resolve => setTimeout(resolve, 10)); // Allow UI to update. - // await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - // assert.deepStrictEqual((await picks)!.map(pick => pick.label), ['zwei', 'drei']); - // }); + test('showQuickPick, keep selection (Microsoft/vscode-azure-account#67)', async function () { + const picks = window.showQuickPick([ + { label: 'eins' }, + { label: 'zwei', picked: true }, + { label: 'drei', picked: true } + ], { + canPickMany: true + }); + await new Promise(resolve => setTimeout(() => resolve(), 100)); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + if (await Promise.race([picks, new Promise(resolve => setTimeout(() => resolve(false), 100))]) === false) { + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + if (await Promise.race([picks, new Promise(resolve => setTimeout(() => resolve(false), 1000))]) === false) { + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + if (await Promise.race([picks, new Promise(resolve => setTimeout(() => resolve(false), 1000))]) === false) { + assert.ok(false, 'Picks not resolved!'); + } + } + } + assert.deepStrictEqual((await picks)!.map(pick => pick.label), ['zwei', 'drei']); + }); test('showQuickPick, undefined on cancel', function () { const source = new CancellationTokenSource(); @@ -521,20 +542,24 @@ suite('vscode API - window', () => { return Promise.all([a, b]); }); - // TODO@chrmarti Disabled due to flaky behaviour (https://github.com/Microsoft/vscode/issues/70887) - // test('showWorkspaceFolderPick', async function () { - // const p = window.showWorkspaceFolderPick(undefined); + test('showWorkspaceFolderPick', async function () { + const p = window.showWorkspaceFolderPick(undefined); - // await timeout(10); - // await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - // try { - // await p; - // assert.ok(true); - // } - // catch (_error) { - // assert.ok(false); - // } - // }); + await new Promise(resolve => setTimeout(resolve, 10)); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + const r1 = await Promise.race([p, new Promise(resolve => setTimeout(() => resolve(false), 100))]); + if (r1 !== false) { + return; + } + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + const r2 = await Promise.race([p, new Promise(resolve => setTimeout(() => resolve(false), 1000))]); + if (r2 !== false) { + return; + } + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + const r3 = await Promise.race([p, new Promise(resolve => setTimeout(() => resolve(false), 1000))]); + assert.ok(r3 !== false); + }); test('Default value for showInput Box not accepted when it fails validateInput, reversing #33691', async function () { const result = window.showInputBox({ @@ -551,6 +576,23 @@ suite('vscode API - window', () => { assert.equal(await result, undefined); }); + function createQuickPickTracker() { + const resolves: ((value: T) => void)[] = []; + let done: () => void; + const unexpected = new Promise((resolve, reject) => { + done = () => resolve(); + resolves.push(reject); + }); + return { + onDidSelectItem: (item: T) => resolves.pop()!(item), + nextItem: () => new Promise(resolve => resolves.push(resolve)), + done: () => { + done!(); + return unexpected; + }, + }; + } + test('editor, selection change kind', () => { return workspace.openTextDocument(join(workspace.rootPath || '', './far.js')).then(doc => window.showTextDocument(doc)).then(editor => { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index e6347f90c2a..4ec107c23be 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2, env, UIKind, ShellExecution, TaskExecution } from 'vscode'; +import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2, env, UIKind, ShellExecution, TaskExecution, Terminal, Event } from 'vscode'; // Disable tasks tests: // - Web https://github.com/microsoft/vscode/issues/90528 @@ -28,26 +28,55 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx const taskType: string = 'customTesting'; const taskName = 'First custom task'; let isPseudoterminalClosed = false; + let terminal: Terminal | undefined; + // There's a strict order that should be observed here: + // 1. The terminal opens + // 2. The terminal is written to. + // 3. The terminal is closed. + enum TestOrder { + Start, + TerminalOpened, + TerminalWritten, + TerminalClosed + } + + let testOrder = TestOrder.Start; + disposables.push(window.onDidOpenTerminal(term => { - disposables.push(window.onDidWriteTerminalData(e => { - try { - assert.equal(e.data, 'testing\r\n'); - } catch (e) { - done(e); - } - disposables.push(window.onDidCloseTerminal(() => { - try { - // Pseudoterminal.close should have fired by now, additionally we want - // to make sure all events are flushed before continuing with more tests - assert.ok(isPseudoterminalClosed); - } catch (e) { - done(e); - return; - } - done(); - })); - term.dispose(); - })); + try { + assert.equal(testOrder, TestOrder.Start); + } catch (e) { + done(e); + } + testOrder = TestOrder.TerminalOpened; + terminal = term; + })); + disposables.push(window.onDidWriteTerminalData(e => { + try { + assert.equal(testOrder, TestOrder.TerminalOpened); + testOrder = TestOrder.TerminalWritten; + assert.notEqual(terminal, undefined); + assert.equal(e.data, 'testing\r\n'); + } catch (e) { + done(e); + } + + if (terminal) { + terminal.dispose(); + } + })); + disposables.push(window.onDidCloseTerminal(() => { + try { + assert.equal(testOrder, TestOrder.TerminalWritten); + testOrder = TestOrder.TerminalClosed; + // Pseudoterminal.close should have fired by now, additionally we want + // to make sure all events are flushed before continuing with more tests + assert.ok(isPseudoterminalClosed); + } catch (e) { + done(e); + return; + } + done(); })); disposables.push(tasks.registerTaskProvider(taskType, { provideTasks: () => { @@ -191,5 +220,54 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx taskExecution = await tasks.executeTask(task); }); }); + + // https://github.com/microsoft/vscode/issues/100577 + test('A CustomExecution task can be fetched and executed', () => { + return new Promise(async (resolve, reject) => { + class CustomTerminal implements Pseudoterminal { + private readonly writeEmitter = new EventEmitter(); + public readonly onDidWrite: Event = this.writeEmitter.event; + public async close(): Promise { } + public open(): void { + this.close(); + resolve(); + } + } + + function buildTask(): Task { + const task = new Task( + { + type: 'customTesting', + }, + TaskScope.Workspace, + 'Test Task', + 'customTesting', + new CustomExecution( + async (): Promise => { + return new CustomTerminal(); + } + ) + ); + return task; + } + + disposables.push(tasks.registerTaskProvider('customTesting', { + provideTasks: () => { + return [buildTask()]; + }, + resolveTask(_task: Task): undefined { + return undefined; + } + })); + + const task = await tasks.fetchTasks({ type: 'customTesting' }); + + if (task && task.length > 0) { + await tasks.executeTask(task[0]); + } else { + reject('fetched task can\'t be undefined'); + } + }); + }); }); }); diff --git a/package.json b/package.json index 2e49bc7ad51..f80b4dc7e09 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.47.0", - "distro": "6eb887883773e6b33879837bdf8dda3340a9fa75", + "distro": "1103b7245defa373835ccc26e0825faf86f7ae5d", "author": { "name": "Microsoft Corporation" }, @@ -152,7 +152,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^4.0.0-dev.20200615", + "typescript": "^4.0.0-dev.20200622", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", @@ -179,4 +179,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} +} \ No newline at end of file diff --git a/product.json b/product.json index 8184879d307..2a176cfb11e 100644 --- a/product.json +++ b/product.json @@ -60,7 +60,7 @@ }, { "name": "ms-vscode.references-view", - "version": "0.0.58", + "version": "0.0.59", "repo": "https://github.com/Microsoft/vscode-reference-view", "metadata": { "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index e85625efd2d..06973937f14 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -1,28 +1,36 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # test that VSCode wasn't installed inside WSL if grep -qi Microsoft /proc/version && [ -z "$DONT_PROMPT_WSL_INSTALL" ]; then - echo "To use VS Code with the Windows Subsystem for Linux, please install VS Code in Windows and uninstall the Linux version in WSL. You can then use the '@@PRODNAME@@' command in a WSL terminal just as you would in a normal command prompt." 1>&2 - read -e -p "Do you want to continue anyways ? [y/N] " YN - - [[ $YN == "n" || $YN == "N" || $YN == "" ]] && exit 1 - echo "To no longer see this prompt, start @@PRODNAME@@ with the environment variable DONT_PROMPT_WSL_INSTALL defined." + echo "To use @@PRODNAME@@ with the Windows Subsystem for Linux, please install @@PRODNAME@@ in Windows and uninstall the Linux version in WSL. You can then use the \`@@NAME@@\` command in a WSL terminal just as you would in a normal command prompt." 1>&2 + printf "Do you want to continue anyway? [y/N] " 1>&2 + read -r YN + YN=$(printf '%s' "$YN" | tr '[:upper:]' '[:lower:]') + case "$YN" in + y | yes ) + ;; + * ) + exit 1 + ;; + esac + echo "To no longer see this prompt, start @@PRODNAME@@ with the environment variable DONT_PROMPT_WSL_INSTALL defined." 1>&2 fi - # If root, ensure that --user-data-dir or --file-write is specified if [ "$(id -u)" = "0" ]; then - for i in $@ + for i in "$@" do - if [[ $i == --user-data-dir || $i == --user-data-dir=* || $i == --file-write ]]; then - CAN_LAUNCH_AS_ROOT=1 - fi + case "$i" in + --user-data-dir | --user-data-dir=* | --file-write ) + CAN_LAUNCH_AS_ROOT=1 + ;; + esac done if [ -z $CAN_LAUNCH_AS_ROOT ]; then - echo "You are trying to start vscode as a super user which is not recommended. If you really want to, you must specify an alternate user data directory using the --user-data-dir argument." 1>&2 + echo "You are trying to start @@PRODNAME@@ as a super user which isn't recommended. If this was intended, please specify an alternate user data directory using the \`--user-data-dir\` argument." 1>&2 exit 1 fi fi @@ -33,7 +41,7 @@ if [ ! -L "$0" ]; then else if command -v readlink >/dev/null; then # if readlink exists, follow the symlink and find relatively - VSCODE_PATH="$(dirname $(readlink -f "$0"))/.." + VSCODE_PATH="$(dirname "$(readlink -f "$0")")/.." else # else use the standard install location VSCODE_PATH="/usr/share/@@NAME@@" diff --git a/resources/serverless/code-web.js b/resources/serverless/code-web.js index 3f35bea7bc8..893844cf92d 100644 --- a/resources/serverless/code-web.js +++ b/resources/serverless/code-web.js @@ -61,10 +61,6 @@ const exists = (path) => util.promisify(fs.exists)(path); const readFile = (path) => util.promisify(fs.readFile)(path); const CharCode_PC = '%'.charCodeAt(0); -function toStaticExtensionUri(path) { - return { scheme: SCHEME, authority: AUTHORITY, path: `/static-extension/${path}` }; -} - async function initialize() { const builtinExtensions = []; const webpackConfigs = []; @@ -84,9 +80,9 @@ async function initialize() { } const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; - const readmeUrl = readme ? toStaticExtensionUri(path.join(extensionPath, readme)) : undefined; + const readmePath = readme ? path.join(extensionPath, readme) : undefined; const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; - const changelogUrl = changelog ? toStaticExtensionUri(path.join(extensionPath, changelog)) : undefined; + const changelogPath = changelog ? path.join(extensionPath, changelog) : undefined; const packageJSONPath = path.join(EXTENSIONS_ROOT, folderName, 'package.json'); if (await exists(packageJSONPath)) { @@ -122,11 +118,11 @@ async function initialize() { const packageNLSPath = path.join(folderName, 'package.nls.json'); const packageNLSExists = await exists(path.join(EXTENSIONS_ROOT, packageNLSPath)); builtinExtensions.push({ + extensionPath: folderName, packageJSON, - location: toStaticExtensionUri(folderName), - packageNLSUrl: packageNLSExists ? toStaticExtensionUri(packageNLSPath) : undefined, - readmeUrl, - changelogUrl + packageNLSPath: packageNLSExists ? packageNLSPath : undefined, + readmePath, + changelogPath }); } catch (e) { console.log(e); @@ -271,6 +267,7 @@ async function handleRoot(req, res) { folderUri: ghPath ? { scheme: 'github', authority: 'HEAD', path: ghPath } : { scheme: 'memfs', path: `/sample-folder` }, + builtinExtensionsServiceUrl: `${SCHEME}://${AUTHORITY}/static-extension` })); const data = (await util.promisify(fs.readFile)(WEB_MAIN)).toString() diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index 4fbdd0a3b51..9f029e5522a 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -13,40 +13,46 @@ NAME="@@NAME@@" DATAFOLDER="@@DATAFOLDER@@" VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" ELECTRON="$VSCODE_PATH/$NAME.exe" -if grep -qi Microsoft /proc/version; then - # in a wsl shell - WSL_BUILD=$(uname -r | sed -E 's/^[0-9.]+-([0-9]+)-Microsoft.*|([0-9]+).([0-9]+).([0-9]+)-microsoft.*|.*/\1\2\3\4/') - if [ -z "$WSL_BUILD" ]; then - WSL_BUILD=0 - fi - if [ $WSL_BUILD -ge 17063 ]; then - # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 - # WSLPATH is available since WSL build 17046 - # WSLENV is available since WSL build 17063 - export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV - CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") - - # use the Remote WSL extension if installed - WSL_EXT_ID="ms-vscode-remote.remote-wsl" - - ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID >/tmp/remote-wsl-loc.txt 2>/dev/null - WSL_EXT_WLOC=$(cat /tmp/remote-wsl-loc.txt) - - if [ -n "$WSL_EXT_WLOC" ]; then - # replace \r\n with \n in WSL_EXT_WLOC - WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh - "$WSL_CODE" "$COMMIT" "$QUALITY" "$ELECTRON" "$APP_NAME" "$DATAFOLDER" "$@" +IN_WSL=false +if [ -n "$WSL_DISTRO_NAME" ]; then + # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 + IN_WSL=true +else + WSL_BUILD=$(uname -r | sed -E 's/^[0-9.]+-([0-9]+)-Microsoft.*|.*/\1/') + if [ -n "$WSL_BUILD" ]; then + if [ "$WSL_BUILD" -ge 17063 ]; then + # WSLPATH is available since WSL build 17046 + # WSLENV is available since WSL build 17063 + IN_WSL=true + else + # If running under older WSL, don't pass cli.js to Electron as + # environment vars cannot be transferred from WSL to Windows + # See: https://github.com/Microsoft/BashOnWindows/issues/1363 + # https://github.com/Microsoft/BashOnWindows/issues/1494 + "$ELECTRON" "$@" exit $? fi - else - # If running under older WSL, don't pass cli.js to Electron as - # environment vars cannot be transferred from WSL to Windows - # See: https://github.com/Microsoft/BashOnWindows/issues/1363 - # https://github.com/Microsoft/BashOnWindows/issues/1494 - "$ELECTRON" "$@" + fi +fi +if [ $IN_WSL = true ]; then + + export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV + CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") + + # use the Remote WSL extension if installed + WSL_EXT_ID="ms-vscode-remote.remote-wsl" + + ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID >/tmp/remote-wsl-loc.txt 2>/dev/null + WSL_EXT_WLOC=$(cat /tmp/remote-wsl-loc.txt) + + if [ -n "$WSL_EXT_WLOC" ]; then + # replace \r\n with \n in WSL_EXT_WLOC + WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh + "$WSL_CODE" "$COMMIT" "$QUALITY" "$ELECTRON" "$APP_NAME" "$DATAFOLDER" "$@" exit $? fi + elif [ -x "$(command -v cygpath)" ]; then CLI=$(cygpath -m "$VSCODE_PATH/resources/app/out/cli.js") else diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 3db8513654c..90a32ae6511 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -9,13 +9,13 @@ const bootstrap = require('./bootstrap'); // Remove global paths from the node module lookup -bootstrap.removeGlobalNodeModuleLookupPaths(); +removeGlobalNodeModuleLookupPaths(); // Enable ASAR in our forked processes bootstrap.enableASARSupport(); if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) { - bootstrap.injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); + injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); } // Configure: pipe logging to parent process @@ -41,6 +41,59 @@ require('./bootstrap-amd').load(process.env['AMD_ENTRYPOINT']); //#region Helpers +/** + * Add support for redirecting the loading of node modules + * + * @param {string} injectPath + */ +function injectNodeModuleLookupPath(injectPath) { + if (!injectPath) { + throw new Error('Missing injectPath'); + } + + const Module = require('module'); + const path = require('path'); + + const nodeModulesPath = path.join(__dirname, '../node_modules'); + + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; + + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent) { + const paths = originalResolveLookupPaths(moduleName, parent); + if (Array.isArray(paths)) { + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === nodeModulesPath) { + paths.splice(i, 0, injectPath); + break; + } + } + } + + return paths; + }; +} + +function removeGlobalNodeModuleLookupPaths() { + const Module = require('module'); + // @ts-ignore + const globalPaths = Module.globalPaths; + + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; + + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent) { + const paths = originalResolveLookupPaths(moduleName, parent); + let commonSuffixLength = 0; + while (commonSuffixLength < paths.length && paths[paths.length - 1 - commonSuffixLength] === globalPaths[globalPaths.length - 1 - commonSuffixLength]) { + commonSuffixLength++; + } + return paths.slice(0, paths.length - commonSuffixLength); + }; +} + function pipeLoggingToParent() { const MAX_LENGTH = 100000; diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 3140e2b2522..5c83975b0c0 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -8,15 +8,6 @@ const bootstrap = require('./bootstrap'); -/** - * @param {object} destination - * @param {object} source - * @returns {object} - */ -exports.assign = function assign(destination, source) { - return Object.keys(source).reduce(function (r, key) { r[key] = source[key]; return r; }, destination); -}; - /** * @param {string[]} modulePaths * @param {(result, configuration: object) => any} resultCallback @@ -59,7 +50,7 @@ exports.load = function (modulePaths, resultCallback, options) { } // Correctly inherit the parent's environment - exports.assign(process.env, configuration.userEnv); + Object.assign(process.env, configuration.userEnv); // Enable ASAR support bootstrap.enableASARSupport(path.join(configuration.appRoot, 'node_modules')); diff --git a/src/bootstrap.js b/src/bootstrap.js index a04f0d72f20..a17560bfbfc 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -19,62 +19,6 @@ process.on('SIGPIPE', () => { //#endregion -//#region Add support for redirecting the loading of node modules - -exports.injectNodeModuleLookupPath = function (injectPath) { - if (!injectPath) { - throw new Error('Missing injectPath'); - } - - const Module = require('module'); - const path = require('path'); - - const nodeModulesPath = path.join(__dirname, '../node_modules'); - - // @ts-ignore - const originalResolveLookupPaths = Module._resolveLookupPaths; - - // @ts-ignore - Module._resolveLookupPaths = function (moduleName, parent) { - const paths = originalResolveLookupPaths(moduleName, parent); - if (Array.isArray(paths)) { - for (let i = 0, len = paths.length; i < len; i++) { - if (paths[i] === nodeModulesPath) { - paths.splice(i, 0, injectPath); - break; - } - } - } - - return paths; - }; -}; - -//#endregion - -//#region Remove global paths from the node lookup paths - -exports.removeGlobalNodeModuleLookupPaths = function () { - const Module = require('module'); - // @ts-ignore - const globalPaths = Module.globalPaths; - - // @ts-ignore - const originalResolveLookupPaths = Module._resolveLookupPaths; - - // @ts-ignore - Module._resolveLookupPaths = function (moduleName, parent) { - const paths = originalResolveLookupPaths(moduleName, parent); - let commonSuffixLength = 0; - while (commonSuffixLength < paths.length && paths[paths.length - 1 - commonSuffixLength] === globalPaths[globalPaths.length - 1 - commonSuffixLength]) { - commonSuffixLength++; - } - return paths.slice(0, paths.length - commonSuffixLength); - }; -}; - -//#endregion - //#region Add support for using node_modules.asar /** @@ -139,57 +83,6 @@ exports.uriFromPath = function (_path) { //#endregion -//#region FS helpers - -/** - * @param {string} file - * @returns {Promise} - */ -exports.readFile = function (file) { - const fs = require('fs'); - - return new Promise(function (resolve, reject) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - reject(err); - return; - } - resolve(data); - }); - }); -}; - -/** - * @param {string} file - * @param {string} content - * @returns {Promise} - */ -exports.writeFile = function (file, content) { - const fs = require('fs'); - - return new Promise(function (resolve, reject) { - fs.writeFile(file, content, 'utf8', function (err) { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); -}; - -/** - * @param {string} dir - * @returns {Promise} - */ -exports.mkdirp = function mkdirp(dir) { - const fs = require('fs'); - - return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir))); -}; - -//#endregion - //#region NLS helpers /** @@ -220,7 +113,7 @@ exports.setupNLS = function () { } const bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, bundle.replace(/\//g, '!') + '.nls.json'); - exports.readFile(bundleFile).then(function (content) { + readFile(bundleFile).then(function (content) { const json = JSON.parse(content); bundles[bundle] = json; @@ -228,7 +121,7 @@ exports.setupNLS = function () { }).catch((error) => { try { if (nlsConfig._corruptedFile) { - exports.writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); + writeFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); } } finally { cb(error, undefined); @@ -240,6 +133,27 @@ exports.setupNLS = function () { return nlsConfig; }; +/** + * @param {string} file + * @returns {Promise} + */ +function readFile(file) { + const fs = require('fs'); + + return fs.promises.readFile(file, 'utf8'); +} + +/** + * @param {string} file + * @param {string} content + * @returns {Promise} + */ +function writeFile(file, content) { + const fs = require('fs'); + + return fs.promises.writeFile(file, content, 'utf8'); +} + //#endregion //#region Portable helpers diff --git a/src/main.js b/src/main.js index 3ce8dcbe443..81e4192e3af 100644 --- a/src/main.js +++ b/src/main.js @@ -86,11 +86,10 @@ setCurrentWorkingDirectory(); // Register custom schemes with privileges protocol.registerSchemesAsPrivileged([ { - scheme: 'vscode-resource', + scheme: 'vscode-webview', privileges: { + standard: true, secure: true, - supportFetchAPI: true, - corsEnabled: true, } }, { scheme: 'vscode-webview-resource', @@ -466,7 +465,7 @@ function getNodeCachedDir() { async ensureExists() { try { - await bootstrap.mkdirp(this.value); + await mkdirp(this.value); return this.value; } catch (error) { @@ -495,6 +494,18 @@ function getNodeCachedDir() { }; } +/** + * @param {string} dir + * @returns {Promise} + */ +function mkdirp(dir) { + const fs = require('fs'); + + return new Promise((resolve, reject) => { + fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? reject(err) : resolve(dir)); + }); +} + //#region NLS Support /** diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index a8105574ac3..db736d5da23 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -10,7 +10,7 @@ import { tail2 as tail, equals } from 'vs/base/common/arrays'; import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, IGridViewOptions, IBoundarySashes } from './gridview'; import { Event } from 'vs/base/common/event'; -export { Orientation, Sizing as GridViewSizing, IViewSize, orthogonal, LayoutPriority } from './gridview'; +export { Orientation, IViewSize, orthogonal, LayoutPriority } from './gridview'; export const enum Direction { Up, diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 9badee8eb96..84682b490f7 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -113,23 +113,21 @@ export class ToolBar extends Disposable { this.actionBar.setAriaLabel(label); } - setActions(primaryActions: ReadonlyArray, secondaryActions?: ReadonlyArray): () => void { - return () => { - let primaryActionsToSet = primaryActions ? primaryActions.slice(0) : []; + setActions(primaryActions: ReadonlyArray, secondaryActions?: ReadonlyArray): void { + let primaryActionsToSet = primaryActions ? primaryActions.slice(0) : []; - // Inject additional action to open secondary actions if present - this.hasSecondaryActions = !!(secondaryActions && secondaryActions.length > 0); - if (this.hasSecondaryActions && secondaryActions) { - this.toggleMenuAction.menuActions = secondaryActions.slice(0); - primaryActionsToSet.push(this.toggleMenuAction); - } + // Inject additional action to open secondary actions if present + this.hasSecondaryActions = !!(secondaryActions && secondaryActions.length > 0); + if (this.hasSecondaryActions && secondaryActions) { + this.toggleMenuAction.menuActions = secondaryActions.slice(0); + primaryActionsToSet.push(this.toggleMenuAction); + } - this.actionBar.clear(); + this.actionBar.clear(); - primaryActionsToSet.forEach(action => { - this.actionBar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) }); - }); - }; + primaryActionsToSet.forEach(action => { + this.actionBar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) }); + }); } private getKeybindingLabel(action: IAction): string | undefined { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 4e8f3348de1..1286c5117a4 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -62,6 +62,14 @@ export namespace Schemas { export const webviewPanel = 'webview-panel'; + /** + * Scheme used for loading the wrapper html and script in webviews. + */ + export const vscodeWebview = 'vscode-webview'; + + /** + * Scheme used for loading resources inside of webviews. + */ export const vscodeWebviewResource = 'vscode-webview-resource'; /** diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index b91569fe60b..d3b0f94da13 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -10,12 +10,12 @@ - - - + + + diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index c68730498de..eac3be20822 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -13,9 +13,6 @@ - - - @@ -23,8 +20,6 @@ - - diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 9badbd9fd32..522b9e2a925 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -182,7 +182,7 @@ function getLazyEnv() { ipc.once('vscode:acceptShellEnv', function (event, shellEnv) { clearTimeout(handle); - bootstrapWindow.assign(process.env, shellEnv); + Object.assign(process.env, shellEnv); // @ts-ignore resolve(process.env); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index aa4fa2727d4..f33fdd8c866 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -172,11 +172,12 @@ export class CodeApplication extends Disposable { return false; } - if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%20role%3D%22document%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') { - return true; + const uri = URI.parse(source); + if (uri.scheme === Schemas.vscodeWebview) { + return uri.path === '/index.html'; } - const srcUri = URI.parse(source).fsPath.toLowerCase(); + const srcUri = uri.fsPath.toLowerCase(); const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase(); return srcUri.startsWith(rootUri + sep); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 32cbd06f69e..e5a0b42db93 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -2011,5 +2011,5 @@ registerThemingParticipant((theme, collector) => { } const deprecatedForeground = theme.getColor(editorForeground) || 'inherit'; - collector.addRule(`.monaco-editor .${ClassName.EditorDeprecatedInlineDecoration} { text-decoration: line-through; text-decoration-color: ${deprecatedForeground}}`); + collector.addRule(`.monaco-editor.showDeprecated .${ClassName.EditorDeprecatedInlineDecoration} { text-decoration: line-through; text-decoration-color: ${deprecatedForeground}}`); }); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 90511cd1879..f5c0a077c63 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -594,6 +594,10 @@ export interface IEditorOptions { * Defaults to false. */ definitionLinkOpensInPeek?: boolean; + /** + * Controls strikethrough deprecated variables. + */ + showDeprecated?: boolean; } export interface IEditorConstructionOptions extends IEditorOptions { @@ -1213,22 +1217,28 @@ class EditorClassName extends ComputedEditorOption 0xFFFF) { + this._searchRegex.lastIndex += 2; + } else { + this._searchRegex.lastIndex += 1; + } continue; } // Exit early if the regex matches the same range twice diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 49e24e9eac5..b1832df3859 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -279,11 +279,12 @@ export enum EditorOption { wordWrapMinified = 109, wrappingIndent = 110, wrappingStrategy = 111, - editorClassName = 112, - pixelRatio = 113, - tabFocusMode = 114, - layoutInfo = 115, - wrappingInfo = 116 + showDeprecated = 112, + editorClassName = 113, + pixelRatio = 114, + tabFocusMode = 115, + layoutInfo = 116, + wrappingInfo = 117 } /** diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 3e72f30d0de..0964eac915f 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -210,12 +210,12 @@ export class ViewLayout extends Disposable implements IViewLayout { const width = layoutInfo.contentWidth; const height = layoutInfo.height; const scrollDimensions = this._scrollable.getScrollDimensions(); - const scrollWidth = scrollDimensions.scrollWidth; + const contentWidth = scrollDimensions.contentWidth; this._scrollable.setScrollDimensions(new EditorScrollDimensions( width, scrollDimensions.contentWidth, height, - this._getContentHeight(width, height, scrollWidth) + this._getContentHeight(width, height, contentWidth) )); } else { this._updateHeight(); @@ -250,14 +250,14 @@ export class ViewLayout extends Disposable implements IViewLayout { return scrollbar.horizontalScrollbarSize; } - private _getContentHeight(width: number, height: number, scrollWidth: number): number { + private _getContentHeight(width: number, height: number, contentWidth: number): number { const options = this._configuration.options; let result = this._linesLayout.getLinesTotalHeight(); if (options.get(EditorOption.scrollBeyondLastLine)) { result += height - options.get(EditorOption.lineHeight); } else { - result += this._getHorizontalScrollbarHeight(width, scrollWidth); + result += this._getHorizontalScrollbarHeight(width, contentWidth); } return result; @@ -267,12 +267,12 @@ export class ViewLayout extends Disposable implements IViewLayout { const scrollDimensions = this._scrollable.getScrollDimensions(); const width = scrollDimensions.width; const height = scrollDimensions.height; - const scrollWidth = scrollDimensions.scrollWidth; + const contentWidth = scrollDimensions.contentWidth; this._scrollable.setScrollDimensions(new EditorScrollDimensions( width, scrollDimensions.contentWidth, height, - this._getContentHeight(width, height, scrollWidth) + this._getContentHeight(width, height, contentWidth) )); } diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index b9e30b80525..f54e722566e 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -73,6 +73,9 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet { } } + +const emptyCodeActionsResponse = { actions: [] as modes.CodeAction[], documentation: undefined }; + export function getCodeActions( model: ITextModel, rangeOrSelection: Range | Selection, @@ -100,7 +103,7 @@ export function getCodeActions( } if (cts.token.isCancellationRequested) { - return { actions: [] as modes.CodeAction[], documentation: undefined }; + return emptyCodeActionsResponse; } const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action)); @@ -111,7 +114,7 @@ export function getCodeActions( throw err; } onUnexpectedExternalError(err); - return { actions: [] as modes.CodeAction[], documentation: undefined }; + return emptyCodeActionsResponse; } }); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index e5af37e781b..715cd3032b5 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -781,4 +781,29 @@ suite('TextModelSearch', () => { model.dispose(); }); + + test('issue #100134. Zero-length matches should properly step over surrogate pairs', () => { + // 1[Laptop]1 - there shoud be no matches inside of [Laptop] emoji + assertFindMatches('1\uD83D\uDCBB1', '()', true, false, null, + [ + [1, 1, 1, 1], + [1, 2, 1, 2], + [1, 4, 1, 4], + [1, 5, 1, 5], + + ] + ); + // 1[Hacker Cat]1 = 1[Cat Face][ZWJ][Laptop]1 - there shoud be matches between emoji and ZWJ + // there shoud be no matches inside of [Cat Face] and [Laptop] emoji + assertFindMatches('1\uD83D\uDC31\u200D\uD83D\uDCBB1', '()', true, false, null, + [ + [1, 1, 1, 1], + [1, 2, 1, 2], + [1, 4, 1, 4], + [1, 5, 1, 5], + [1, 7, 1, 7], + [1, 8, 1, 8] + ] + ); + }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 947bdf93a47..8407dfa7ab7 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3129,6 +3129,10 @@ declare namespace monaco.editor { * Defaults to false. */ definitionLinkOpensInPeek?: boolean; + /** + * Controls strikethrough deprecated variables. + */ + showDeprecated?: boolean; } export interface IEditorConstructionOptions extends IEditorOptions { @@ -3945,11 +3949,12 @@ declare namespace monaco.editor { wordWrapMinified = 109, wrappingIndent = 110, wrappingStrategy = 111, - editorClassName = 112, - pixelRatio = 113, - tabFocusMode = 114, - layoutInfo = 115, - wrappingInfo = 116 + showDeprecated = 112, + editorClassName = 113, + pixelRatio = 114, + tabFocusMode = 115, + layoutInfo = 116, + wrappingInfo = 117 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4045,6 +4050,7 @@ declare namespace monaco.editor { selectOnLineNumbers: IEditorOption; showFoldingControls: IEditorOption; showUnused: IEditorOption; + showDeprecated: IEditorOption; snippetSuggestions: IEditorOption; smoothScrolling: IEditorOption; stopRenderingLineAfter: IEditorOption; diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index e467824f42a..4fc56a95e90 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -327,7 +327,7 @@ export class ElectronMainService implements IElectronMainService { } async writeClipboardBuffer(windowId: number | undefined, format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise { - return clipboard.writeBuffer(format, buffer as Buffer, type); + return clipboard.writeBuffer(format, Buffer.from(buffer), type); } async readClipboardBuffer(windowId: number | undefined, format: string): Promise { diff --git a/src/vs/platform/extensions/browser/builtinExtensionsScannerService.ts b/src/vs/platform/extensions/browser/builtinExtensionsScannerService.ts deleted file mode 100644 index 74a0c367750..00000000000 --- a/src/vs/platform/extensions/browser/builtinExtensionsScannerService.ts +++ /dev/null @@ -1,44 +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 { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { isWeb } from 'vs/base/common/platform'; -import { URI } from 'vs/base/common/uri'; - -export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScannerService { - - declare readonly _serviceBrand: undefined; - - private readonly builtinExtensions: IScannedExtension[] = []; - - constructor( - ) { - if (isWeb) { - // Find builtin extensions by checking for DOM - const builtinExtensionsElement = document.getElementById('vscode-workbench-builtin-extensions'); - const builtinExtensionsElementAttribute = builtinExtensionsElement ? builtinExtensionsElement.getAttribute('data-settings') : undefined; - if (builtinExtensionsElementAttribute) { - try { - const builtinExtensions: IScannedExtension[] = JSON.parse(builtinExtensionsElementAttribute); - this.builtinExtensions = builtinExtensions.map(e => { - location: URI.revive(e.location), - type: ExtensionType.System, - packageJSON: e.packageJSON, - packageNLSUrl: URI.revive(e.packageNLSUrl), - readmeUrl: URI.revive(e.readmeUrl), - changelogUrl: URI.revive(e.changelogUrl), - }); - } catch (error) { /* ignore error*/ } - } - } - } - - async scanBuiltinExtensions(): Promise { - if (isWeb) { - return this.builtinExtensions; - } - throw new Error('not supported'); - } -} diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index 8da70a013af..dd0d470a1b3 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -23,6 +23,10 @@ interface WebviewMetadata { export class WebviewProtocolProvider extends Disposable { + private static validWebviewFilePaths = new Map([ + ['/index.html', 'index.html'], + ]); + private readonly webviewMetadata = new Map(); constructor( @@ -87,8 +91,23 @@ export class WebviewProtocolProvider extends Disposable { return callback({ data: null, statusCode: 404 }); }); - this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource))); + + + sess.protocol.registerFileProtocol(Schemas.vscodeWebview, (request, callback: any) => { + try { + const uri = URI.parse(request.url); + const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); + if (typeof entry === 'string') { + const url = require.toUrl(`vs/workbench/contrib/webview/electron-browser/pre/${entry}`); + return callback(url.replace('file://', '')); + } + } catch { + // noop + } + callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ }); + }); + this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebview))); } private streamToNodeReadable(stream: VSBufferReadableStream): Readable { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 7e6ad3eab46..4e4ff3959da 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2228,6 +2228,40 @@ declare module 'vscode' { * such as `[CodeActionKind.Refactor.Extract.append('function'), CodeActionKind.Refactor.Extract.append('constant'), ...]`. */ readonly providedCodeActionKinds?: ReadonlyArray; + + /** + * Static documentation for a class of code actions. + * + * Documentation from the provider is shown in the code actions menu if either: + * + * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * most closely matches the requested code action kind. For example, if a provider has documentation for + * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, + * VS Code will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. + * + * - Any code actions of `kind` are returned by the provider. + * + * At most one documentation entry will be shown per provider. + */ + readonly documentation?: ReadonlyArray<{ + /** + * The kind of the code action being documented. + * + * If the kind is generic, such as `CodeActionKind.Refactor`, the documentation will be shown whenever any + * refactorings are returned. If the kind if more specific, such as `CodeActionKind.RefactorExtract`, the + * documentation will only be shown when extract refactoring code actions are returned. + */ + readonly kind: CodeActionKind; + + /** + * Command that displays the documentation to the user. + * + * This can display the documentation directly in VS Code or open a website using [`env.openExternal`](#env.openExternal); + * + * The title of this documentation code action is taken from [`Command.title`](#Command.title) + */ + readonly command: Command; + }>; } /** @@ -9315,7 +9349,7 @@ declare module 'vscode' { /** * A workspace folder is one of potentially many roots opened by the editor. All workspace folders - * are equal which means there is no notion of an active or master workspace folder. + * are equal which means there is no notion of an active or primary workspace folder. */ export interface WorkspaceFolder { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index f849ec11761..8c453a79eb1 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from 'vscode'; + /** * This is the place for API experiments and proposals. * These API are NOT stable and subject to change. They are only available in the Insiders @@ -1154,6 +1156,11 @@ declare module 'vscode' { */ label?: string | TreeItemLabel | /* for compilation */ any; + /** + * Content to be shown when you hover over the tree item. + */ + tooltip?: string | MarkdownString | /* for compilation */ any; + /** * Accessibility information used when screen reader interacts with this tree item. * Generally, a TreeItem has no need to set the `role` of the accessibilityInformation; @@ -1600,6 +1607,20 @@ declare module 'vscode' { */ render(document: NotebookDocument, request: NotebookRenderRequest): string; + /** + * Call before HTML from the renderer is executed, and will be called for + * every editor associated with notebook documents where the renderer + * is or was used. + * + * The communication object will only send and receive messages to the + * render API, retrieved via `acquireNotebookRendererApi`, acquired with + * this specific renderer's ID. + * + * If you need to keep an association between the communication object + * and the document for use in the `render()` method, you can use a WeakMap. + */ + resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; + readonly preloads?: Uri[]; } @@ -1728,27 +1749,41 @@ declare module 'vscode' { readonly backupId?: string; } + /** + * Communication object passed to the {@link NotebookContentProvider} and + * {@link NotebookOutputRenderer} to communicate with the webview. + */ + export interface NotebookCommunication { + /** + * ID of the editor this object communicates with. A single notebook + * document can have multiple attached webviews and editors, when the + * notebook is split for instance. The editor ID lets you differentiate + * between them. + */ + readonly editorId: string; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + } + export interface NotebookContentProvider { openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise; - resolveNotebook(document: NotebookDocument, webview: { - /** - * Fired when the output hosting webview posts a message. - */ - readonly onDidReceiveMessage: Event; - /** - * Post a message to the output hosting webview. - * - * Messages are only delivered if the editor is live. - * - * @param message Body of the message. This must be a string or other json serilizable object. - */ - postMessage(message: any): Thenable; - - /** - * Convert a uri for the local file system to one that can be used inside outputs webview. - */ - asWebviewUri(localResource: Uri): Uri; - }): Promise; + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise; saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; readonly onDidChangeNotebook: Event; @@ -2001,26 +2036,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/86788 - - export interface CodeActionProviderMetadata { - /** - * Static documentation for a class of code actions. - * - * The documentation is shown in the code actions menu if either: - * - * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that - * most closely matches the requested code action kind. For example, if a provider has documentation for - * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, - * VS Code will use the documentation for `RefactorExtract` intead of the documentation for `Refactor`. - * - * - Any code actions of `kind` are returned by the provider. - */ - readonly documentation?: ReadonlyArray<{ readonly kind: CodeActionKind, readonly command: Command; }>; - } - - //#endregion - //#region Dialog title: https://github.com/microsoft/vscode/issues/82871 /** diff --git a/src/vs/workbench/api/browser/mainThreadDecorations.ts b/src/vs/workbench/api/browser/mainThreadDecorations.ts index 08d46a98405..059479044ba 100644 --- a/src/vs/workbench/api/browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/browser/mainThreadDecorations.ts @@ -9,33 +9,33 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainContext, IExtHostContext, MainThreadDecorationsShape, ExtHostDecorationsShape, DecorationData, DecorationRequest } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IDecorationsService, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; -import { values } from 'vs/base/common/collections'; import { CancellationToken } from 'vs/base/common/cancellation'; class DecorationRequestsQueue { private _idPool = 0; - private _requests: { [id: number]: DecorationRequest } = Object.create(null); - private _resolver: { [id: number]: (data: DecorationData) => any } = Object.create(null); + private _requests = new Map(); + private _resolver = new Map any>(); private _timer: any; constructor( - private readonly _proxy: ExtHostDecorationsShape + private readonly _proxy: ExtHostDecorationsShape, + private readonly _handle: number ) { // } - enqueue(handle: number, uri: URI, token: CancellationToken): Promise { + enqueue(uri: URI, token: CancellationToken): Promise { const id = ++this._idPool; const result = new Promise(resolve => { - this._requests[id] = { id, handle, uri }; - this._resolver[id] = resolve; + this._requests.set(id, { id, uri }); + this._resolver.set(id, resolve); this._processQueue(); }); token.onCancellationRequested(() => { - delete this._requests[id]; - delete this._resolver[id]; + this._requests.delete(id); + this._resolver.delete(id); }); return result; } @@ -49,15 +49,15 @@ class DecorationRequestsQueue { // make request const requests = this._requests; const resolver = this._resolver; - this._proxy.$provideDecorations(values(requests), CancellationToken.None).then(data => { + this._proxy.$provideDecorations(this._handle, [...requests.values()], CancellationToken.None).then(data => { for (const id in resolver) { - resolver[id](data[id]); + resolver.get(Number(id))!(data[Number(id)]); } }); // reset - this._requests = []; - this._resolver = []; + this._requests = new Map(); + this._resolver = new Map(); this._timer = undefined; }, 0); } @@ -68,14 +68,12 @@ export class MainThreadDecorations implements MainThreadDecorationsShape { private readonly _provider = new Map, IDisposable]>(); private readonly _proxy: ExtHostDecorationsShape; - private readonly _requestQueue: DecorationRequestsQueue; constructor( context: IExtHostContext, @IDecorationsService private readonly _decorationsService: IDecorationsService ) { this._proxy = context.getProxy(ExtHostContext.ExtHostDecorations); - this._requestQueue = new DecorationRequestsQueue(this._proxy); } dispose() { @@ -85,23 +83,23 @@ export class MainThreadDecorations implements MainThreadDecorationsShape { $registerDecorationProvider(handle: number, label: string): void { const emitter = new Emitter(); + const queue = new DecorationRequestsQueue(this._proxy, handle); const registration = this._decorationsService.registerDecorationsProvider({ label, onDidChange: emitter.event, - provideDecorations: (uri, token) => { - return this._requestQueue.enqueue(handle, uri, token).then(data => { - if (!data) { - return undefined; - } - const [weight, bubble, tooltip, letter, themeColor] = data; - return { - weight: weight || 0, - bubble: bubble || false, - color: themeColor && themeColor.id, - tooltip, - letter - }; - }); + provideDecorations: async (uri, token) => { + const data = await queue.enqueue(uri, token); + if (!data) { + return undefined; + } + const [weight, bubble, tooltip, letter, themeColor] = data; + return { + weight: weight ?? 0, + bubble: bubble ?? false, + color: themeColor?.id, + tooltip, + letter + }; } }); this._provider.set(handle, [emitter, registration]); diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index d78dc231e8d..e14517d0165 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -253,7 +253,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - this._proxy.$acceptDocumentAndEditorsDelta(delta); + return this._proxy.$acceptDocumentAndEditorsDelta(delta); } registerListeners() { @@ -476,16 +476,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return this._proxy.$executeNotebook(viewType, uri, undefined, useAttachedKernel, token); } - async $postMessage(handle: number, value: any): Promise { - - const activeEditorPane = this.editorService.activeEditorPane as any | undefined; - if (activeEditorPane?.isNotebookEditor) { - const notebookEditor = (activeEditorPane.getControl() as INotebookEditor); - - if (notebookEditor.viewModel?.handle === handle) { - notebookEditor.postMessage(value); - return true; - } + async $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise { + const editor = this._notebookService.getNotebookEditor(editorId) as INotebookEditor | undefined; + if (editor?.isNotebookEditor) { + editor.postMessage(forRendererId, value); + return true; } return false; @@ -645,8 +640,8 @@ export class MainThreadNotebookController implements IMainNotebookController { return this._mainThreadNotebook.executeNotebook(viewType, uri, useAttachedKernel, token); } - onDidReceiveMessage(editorId: string, message: any): void { - this._proxy.$onDidReceiveMessage(editorId, message); + onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: unknown): void { + this._proxy.$onDidReceiveMessage(editorId, rendererType, message); } async removeNotebookDocument(notebook: INotebookTextModel): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 8b37ab49c14..a51b71d9fb2 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -328,10 +328,10 @@ namespace TaskDTO { result.detail = task.configurationProperties.detail; } if (!ConfiguringTask.is(task) && task.command) { - if (task.command.runtime === RuntimeType.Process) { - result.execution = ProcessExecutionDTO.from(task.command); - } else if (task.command.runtime === RuntimeType.Shell) { - result.execution = ShellExecutionDTO.from(task.command); + switch (task.command.runtime) { + case RuntimeType.Process: result.execution = ProcessExecutionDTO.from(task.command); break; + case RuntimeType.Shell: result.execution = ShellExecutionDTO.from(task.command); break; + case RuntimeType.CustomExecution: result.execution = CustomExecutionDTO.from(task.command); break; } } if (task.configurationProperties.problemMatchers) { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 758aa349107..db416f7041f 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -185,6 +185,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma }); } + dispose() { + super.dispose(); + + for (const disposable of this._editorProviders.values()) { + disposable.dispose(); + } + this._editorProviders.clear(); + } + public $createWebviewPanel( extensionData: extHostProtocol.WebviewExtensionDescription, handle: extHostProtocol.WebviewPanelHandle, @@ -320,7 +329,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, supportsMultipleEditorsPerDocument: boolean, - ): DisposableStore { + ): void { if (this._editorProviders.has(viewType)) { throw new Error(`Provider for ${viewType} already registered`); } @@ -396,8 +405,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma })); this._editorProviders.set(viewType, disposables); - - return disposables; } public $unregisterEditorProvider(viewType: string): void { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index df17b498274..a9aadf93a9d 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as resources from 'vs/base/common/resources'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation } from 'vs/workbench/common/views'; -import { TreeViewPane, CustomTreeView } from 'vs/workbench/browser/parts/views/treeView'; +import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { coalesce, } from 'vs/base/common/arrays'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -31,6 +31,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Codicon } from 'vs/base/common/codicons'; +import { CustomTreeView } from 'vs/workbench/contrib/views/browser/treeView'; export interface IUserFriendlyViewsContainerDescriptor { id: string; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0486ac866be..3e8649520e6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -711,7 +711,7 @@ export interface MainThreadNotebookShape extends IDisposable { $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; - $postMessage(handle: number, value: any): Promise; + $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; @@ -1525,7 +1525,6 @@ export interface ExtHostDebugServiceShape { export interface DecorationRequest { readonly id: number; - readonly handle: number; readonly uri: UriComponents; } @@ -1533,7 +1532,7 @@ export type DecorationData = [number, boolean, string, string, ThemeColor]; export type DecorationReply = { [id: number]: DecorationData; }; export interface ExtHostDecorationsShape { - $provideDecorations(requests: DecorationRequest[], token: CancellationToken): Promise; + $provideDecorations(handle: number, requests: DecorationRequest[], token: CancellationToken): Promise; } export interface ExtHostWindowShape { @@ -1610,7 +1609,7 @@ export interface ExtHostNotebookShape { $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; - $onDidReceiveMessage(editorId: string, message: any): void; + $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index 9ed26739e6d..d50d8f15dd2 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -52,17 +52,20 @@ export class ExtHostDecorations implements IExtHostDecorations { }); } - $provideDecorations(requests: DecorationRequest[], token: CancellationToken): Promise { + async $provideDecorations(handle: number, requests: DecorationRequest[], token: CancellationToken): Promise { + + if (!this._provider.has(handle)) { + // might have been unregistered in the meantime + return Object.create(null); + } + const result: DecorationReply = Object.create(null); - return Promise.all(requests.map(request => { - const { handle, uri, id } = request; - const entry = this._provider.get(handle); - if (!entry) { - // might have been unregistered in the meantime - return undefined; - } - const { provider, extensionId } = entry; - return Promise.resolve(provider.provideDecoration(URI.revive(uri), token)).then(data => { + const { provider, extensionId } = this._provider.get(handle)!; + + await Promise.all(requests.map(async request => { + try { + const { uri, id } = request; + const data = await Promise.resolve(provider.provideDecoration(URI.revive(uri), token)); if (!data) { return; } @@ -72,13 +75,12 @@ export class ExtHostDecorations implements IExtHostDecorations { } catch (e) { this._logService.warn(`INVALID decoration from extension '${extensionId.value}': ${e}`); } - }, err => { + } catch (err) { this._logService.error(err); - }); + } + })); - })).then(() => { - return result; - }); + return result; } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index a114726c453..cbe85a2675e 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -73,7 +73,7 @@ class DocumentSymbolAdapter { const element: modes.DocumentSymbol = { name: info.name || '!!MISSING: name!!', kind: typeConvert.SymbolKind.from(info.kind), - tags: info.tags ? info.tags.map(typeConvert.SymbolTag.from) : [], + tags: info.tags?.map(typeConvert.SymbolTag.from) || [], detail: '', containerName: info.containerName, range: typeConvert.Range.from(info.location.range), @@ -1287,6 +1287,7 @@ class CallHierarchyAdapter { uri: item.uri, range: typeConvert.Range.from(item.range), selectionRange: typeConvert.Range.from(item.selectionRange), + tags: item.tags?.map(typeConvert.SymbolTag.from) }; map.set(dto._itemId, item); return dto; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 622aeeb4889..9510132cf92 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -553,27 +553,48 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE } } -class ExtHostWebviewComm extends Disposable { - - onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; +class ExtHostWebviewCommWrapper extends Disposable { + private readonly _onDidReceiveDocumentMessage = new Emitter(); + private readonly _rendererIdToEmitters = new Map>(); constructor( - readonly id: string, + private _editorId: string, public uri: URI, private _proxy: MainThreadNotebookShape, - private _onDidReceiveMessage: Emitter, private _webviewInitData: WebviewInitData, public document: ExtHostNotebookDocument, ) { super(); } - async postMessage(message: any): Promise { - return this._proxy.$postMessage(this.document.handle, message); + public onDidReceiveMessage(forRendererId: string | undefined, message: any) { + this._onDidReceiveDocumentMessage.fire(message); + if (forRendererId !== undefined) { + this._rendererIdToEmitters.get(forRendererId)?.fire(message); + } } - asWebviewUri(localResource: vscode.Uri): vscode.Uri { - return asWebviewUri(this._webviewInitData, this.id, localResource); + public readonly contentProviderComm: vscode.NotebookCommunication = { + editorId: this._editorId, + onDidReceiveMessage: this._onDidReceiveDocumentMessage.event, + postMessage: (message: any) => this._proxy.$postMessage(this._editorId, undefined, message), + asWebviewUri: (uri: vscode.Uri) => this._asWebviewUri(uri), + }; + + public getRendererComm(rendererId: string): vscode.NotebookCommunication { + const emitter = new Emitter(); + this._rendererIdToEmitters.set(rendererId, emitter); + return { + editorId: this._editorId, + onDidReceiveMessage: emitter.event, + postMessage: (message: any) => this._proxy.$postMessage(this._editorId, rendererId, message), + asWebviewUri: (uri: vscode.Uri) => this._asWebviewUri(uri), + }; + } + + + private _asWebviewUri(localResource: vscode.Uri): vscode.Uri { + return asWebviewUri(this._webviewInitData, this._editorId, localResource); } } @@ -618,7 +639,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook readonly id: string, public uri: URI, private _proxy: MainThreadNotebookShape, - private _webComm: ExtHostWebviewComm, + private _webComm: vscode.NotebookCommunication, public document: ExtHostNotebookDocument, private _documentsAndEditors: ExtHostDocumentsAndEditors ) { @@ -721,6 +742,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook export class ExtHostNotebookOutputRenderer { private static _handlePool: number = 0; + private resolvedComms = new WeakSet(); readonly handle = ExtHostNotebookOutputRenderer._handlePool++; constructor( @@ -740,13 +762,19 @@ export class ExtHostNotebookOutputRenderer { return false; } + resolveNotebook(document: ExtHostNotebookDocument, comm: ExtHostWebviewCommWrapper) { + if (!this.resolvedComms.has(comm) && this.renderer.resolveNotebook) { + this.renderer.resolveNotebook(document, comm.getRendererComm(this.type)); + this.resolvedComms.add(comm); + } + } + render(document: ExtHostNotebookDocument, output: vscode.CellDisplayOutput, outputId: string, mimeType: string): string { let html = this.renderer.render(document, { output, outputId, mimeType }); return html; } } - export interface ExtHostNotebookOutputRenderingHandler { outputDisplayOrder: INotebookDisplayOrder | undefined; findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[]; @@ -759,8 +787,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _documents = new Map(); private readonly _unInitializedDocuments = new Map(); private readonly _editors = new Map(); - private readonly _webviewComm = new Map }>(); + private readonly _webviewComm = new Map(); private readonly _notebookOutputRenderers = new Map(); + private readonly _renderersUsedInNotebooks = new WeakMap>(); private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -854,6 +883,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const renderer = this._notebookOutputRenderers.get(id)!; + this.provideCommToNotebookRenderers(document, renderer); + const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { const cell = document.getCell2(cellInfo.key)!; const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { @@ -890,6 +921,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const renderer = this._notebookOutputRenderers.get(id)!; + this.provideCommToNotebookRenderers(document, renderer); + const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { return { @@ -1035,19 +1068,36 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return; } + let webComm = this._webviewComm.get(editorId); + if (!webComm) { + webComm = new ExtHostWebviewCommWrapper(editorId, revivedUri, this._proxy, this._webviewInitData, document); + this._webviewComm.set(editorId, webComm); + } + if (!provider.provider.resolveNotebook) { return; } - let webComm = this._webviewComm.get(editorId)?.comm; + await provider.provider.resolveNotebook(document, webComm.contentProviderComm); + } - if (webComm) { - await provider.provider.resolveNotebook(document, webComm); - } else { - const onDidReceiveMessage = new Emitter(); - webComm = new ExtHostWebviewComm(editorId, revivedUri, this._proxy, onDidReceiveMessage, this._webviewInitData, document); - this._webviewComm.set(editorId, { comm: webComm, onDidReceiveMessage }); - await provider.provider.resolveNotebook(document, webComm); + private provideCommToNotebookRenderers(document: ExtHostNotebookDocument, renderer: ExtHostNotebookOutputRenderer) { + let alreadyRegistered = this._renderersUsedInNotebooks.get(document); + if (!alreadyRegistered) { + alreadyRegistered = new Set(); + this._renderersUsedInNotebooks.set(document, alreadyRegistered); + } + + if (alreadyRegistered.has(renderer)) { + return; + } + + alreadyRegistered.add(renderer); + for (const editorId of this._editors.keys()) { + const comm = this._webviewComm.get(editorId); + if (comm) { + renderer.resolveNotebook(document, comm); + } } } @@ -1182,12 +1232,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return editor; } - $onDidReceiveMessage(editorId: string, message: any): void { - let messageEmitter = this._webviewComm.get(editorId)?.onDidReceiveMessage; - - if (messageEmitter) { - messageEmitter.fire(message); - } + $onDidReceiveMessage(editorId: string, forRendererType: string | undefined, message: any): void { + this._webviewComm.get(editorId)?.onDidReceiveMessage(forRendererType, message); } $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void { @@ -1226,12 +1272,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[]) { const revivedUri = document.uri; - let webComm = this._webviewComm.get(editorId)?.comm; + let webComm = this._webviewComm.get(editorId); if (!webComm) { - const onDidReceiveMessage = new Emitter(); - webComm = new ExtHostWebviewComm(editorId, revivedUri, this._proxy, onDidReceiveMessage, this._webviewInitData, document); - this._webviewComm.set(editorId, { comm: webComm!, onDidReceiveMessage }); + webComm = new ExtHostWebviewCommWrapper(editorId, revivedUri, this._proxy, this._webviewInitData, document); + this._webviewComm.set(editorId, webComm); } let editor = new ExtHostNotebookEditor( @@ -1239,7 +1284,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN editorId, revivedUri, this._proxy, - webComm, + webComm.contentProviderComm, document, this._documentsAndEditors ); @@ -1255,6 +1300,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._editors.get(editorId)?.editor.dispose(); + for (const renderer of this._renderersUsedInNotebooks.get(document) ?? []) { + renderer.resolveNotebook(document, webComm); + } + this._editors.set(editorId, { editor }); } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 67a97da2d52..0ae79328fda 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -19,6 +19,8 @@ import { equals, coalesce } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; type TreeItemHandle = string; @@ -486,6 +488,17 @@ class ExtHostTreeView extends Disposable { return node; } + private getTooltip(tooltip?: string | vscode.MarkdownString): string | IMarkdownString | undefined { + if (typeof tooltip === 'string') { + return tooltip; + } else if (tooltip === undefined) { + return undefined; + } else { + checkProposedApiEnabled(this.extension); + return MarkdownString.from(tooltip); + } + } + private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem2, parent: TreeNode | Root): TreeNode { const disposable = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); @@ -496,7 +509,7 @@ class ExtHostTreeView extends Disposable { label: toTreeItemLabel(extensionTreeItem.label, this.extension), description: extensionTreeItem.description, resourceUri: extensionTreeItem.resourceUri, - tooltip: typeof extensionTreeItem.tooltip === 'string' ? extensionTreeItem.tooltip : undefined, + tooltip: this.getTooltip(extensionTreeItem.tooltip), command: extensionTreeItem.command ? this.commands.toInternal(extensionTreeItem.command, disposable) : undefined, contextValue: extensionTreeItem.contextValue, icon, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6679b7bdd2a..6a7993f9ae7 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -646,7 +646,7 @@ export namespace DocumentSymbol { range: Range.from(info.range), selectionRange: Range.from(info.selectionRange), kind: SymbolKind.from(info.kind), - tags: info.tags ? info.tags.map(SymbolTag.from) : [] + tags: info.tags?.map(SymbolTag.from) ?? [] }; if (info.children) { result.children = info.children.map(from); @@ -911,7 +911,7 @@ export namespace CompletionItem { result.insertText = suggestion.insertText; result.kind = CompletionItemKind.to(suggestion.kind); - result.tags = suggestion.tags && suggestion.tags.map(CompletionItemTag.to); + result.tags = suggestion.tags?.map(CompletionItemTag.to); result.detail = suggestion.detail; result.documentation = htmlContent.isMarkdownString(suggestion.documentation) ? MarkdownString.to(suggestion.documentation) : suggestion.documentation; result.sortText = suggestion.sortText; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index cb1f0c981a7..03bfa693330 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2101,7 +2101,7 @@ export class TreeItem { iconPath?: string | URI | { light: string | URI; dark: string | URI; }; command?: vscode.Command; contextValue?: string; - tooltip?: string; + tooltip?: string | vscode.MarkdownString; constructor(label: string | vscode.TreeItemLabel, collapsibleState?: vscode.TreeItemCollapsibleState); constructor(resourceUri: URI, collapsibleState?: vscode.TreeItemCollapsibleState); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index b785156b037..37776af324e 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -59,7 +59,7 @@ export class ExtHostTask extends ExtHostTaskBase { throw new Error('Task from execution DTO is undefined'); } const execution = await this.getTaskExecution(executionDTO, task); - this._proxy.$executeTask(executionDTO.task).catch(error => { throw new Error(error); }); + this._proxy.$executeTask(executionDTO.task).catch(() => { /* The error here isn't actionable. */ }); return execution; } else { const dto = TaskDTO.from(task, extension); @@ -75,7 +75,7 @@ export class ExtHostTask extends ExtHostTaskBase { } // Always get the task execution first to prevent timing issues when retrieving it later const execution = await this.getTaskExecution(await this._proxy.$getTaskExecution(dto), task); - this._proxy.$executeTask(dto).catch(error => { throw new Error(error); }); + this._proxy.$executeTask(dto).catch(() => { /* The error here isn't actionable. */ }); return execution; } } diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 681b2797784..dd8f4e1fe7c 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -51,7 +51,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // fetch JS sources as text and create a new function around it const source = await response.text(); - const initFn = new Function('module', 'exports', 'require', 'window', `${source}\n//# sourceURL=${module.toString(true)}`); + const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${module.toString(true)}`); // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; @@ -66,7 +66,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { try { activationTimesBuilder.codeLoadingStart(); - initFn(_module, _exports, _require, self); + initFn(_module, _exports, _require); return (_module.exports !== _exports ? _module.exports : _exports); } finally { activationTimesBuilder.codeLoadingStop(); diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 0fc20acc3fe..9d2fe752921 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -263,3 +263,7 @@ body.web { .monaco-workbench .monaco-list:focus { outline: 0 !important; /* tree indicates focus not via outline but through the focused item */ } + +.monaco-workbench .codicon[class*='codicon-'] { + font-size: 16px; /* sets font-size for codicons in workbench https://github.com/microsoft/vscode/issues/98495 */ +} diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 381e835ed03..09a3e5b4ccf 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -331,7 +331,7 @@ export abstract class CompositePart extends Part { toolBar.context = this.actionsContextProvider(); // Return fn to set into toolbar - return toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions)); + return () => toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions)); } protected getActiveComposite(): IComposite | undefined { @@ -392,7 +392,8 @@ export abstract class CompositePart extends Part { actionViewItemProvider: action => this.actionViewItemProvider(action), orientation: ActionsOrientation.HORIZONTAL, getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment() + anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(), + toggleMenuTitle: nls.localize('viewsAndMoreActions', "Views and More Actions...") })); this.collectCompositeActions()(); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index c431dca49a1..c65d738df0b 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -81,7 +81,7 @@ class GridWidgetView implements IView { } } -export class EditorPart extends Part implements IEditorGroupsService, IEditorGroupsAccessor { +export class EditorPart extends Part implements IEditorGroupsService, IEditorGroupsAccessor, IEditorDropService { declare readonly _serviceBrand: undefined; @@ -1141,5 +1141,16 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro //#endregion } +class EditorDropService implements IEditorDropService { + + declare readonly _serviceBrand: undefined; + + constructor(@IEditorGroupsService private readonly editorPart: EditorPart) { } + + createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { + return this.editorPart.createEditorDropTarget(container, delegate); + } +} + registerSingleton(IEditorGroupsService, EditorPart); -registerSingleton(IEditorDropService, EditorPart); +registerSingleton(IEditorDropService, EditorDropService); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index b778ec9d3f2..11ff2e0e8c2 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -191,7 +191,7 @@ export abstract class TitleControl extends Themable { secondaryEditorActions.some(action => action instanceof ExecuteCommandAction) // see also https://github.com/Microsoft/vscode/issues/16298 ) { const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); - editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions)(); + editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); this.currentPrimaryEditorActionIds = primaryEditorActionIds; this.currentSecondaryEditorActionIds = secondaryEditorActionIds; @@ -246,7 +246,7 @@ export abstract class TitleControl extends Themable { protected clearEditorActionsToolbar(): void { if (this.editorActionsToolbar) { - this.editorActionsToolbar.setActions([], [])(); + this.editorActionsToolbar.setActions([], []); } this.currentPrimaryEditorActionIds = []; diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index f6bde386e22..130382bb9a7 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -3,44 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/views'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IAction, ActionRunner } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ITreeItemLabel, Extensions, IViewDescriptorService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import * as DOM from 'vs/base/browser/dom'; -import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; -import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { URI } from 'vs/base/common/uri'; -import { dirname, basename } from 'vs/base/common/resources'; -import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; -import { FileKind } from 'vs/platform/files/common/files'; -import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { localize } from 'vs/nls'; -import { timeout } from 'vs/base/common/async'; -import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; -import { isString } from 'vs/base/common/types'; -import { ILabelService } from 'vs/platform/label/common/label'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; -import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; -import { isFalsyOrWhitespace } from 'vs/base/common/strings'; -import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -81,10 +55,7 @@ export class TreeViewPane extends ViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); - - if (this.treeView instanceof TreeView) { - this.treeView.show(container); - } + this.treeView.show(container); } shouldShowWelcome(): boolean { @@ -104,935 +75,3 @@ export class TreeViewPane extends ViewPane { this.treeView.setVisibility(this.isBodyVisible()); } } - -class Root implements ITreeItem { - label = { label: 'root' }; - handle = '0'; - parentHandle: string | undefined = undefined; - collapsibleState = TreeItemCollapsibleState.Expanded; - children: ITreeItem[] | undefined = undefined; -} - -const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); - -class Tree extends WorkbenchAsyncDataTree { } - -export class TreeView extends Disposable implements ITreeView { - - private isVisible: boolean = false; - private _hasIconForParentNode = false; - private _hasIconForLeafNode = false; - - private readonly collapseAllContextKey: RawContextKey; - private readonly collapseAllContext: IContextKey; - private readonly refreshContextKey: RawContextKey; - private readonly refreshContext: IContextKey; - - private focused: boolean = false; - private domNode!: HTMLElement; - private treeContainer!: HTMLElement; - private _messageValue: string | undefined; - private _canSelectMany: boolean = false; - private messageElement!: HTMLDivElement; - private tree: Tree | undefined; - private treeLabels: ResourceLabels | undefined; - - private root: ITreeItem; - private elementsToRefresh: ITreeItem[] = []; - - private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); - readonly onDidExpandItem: Event = this._onDidExpandItem.event; - - private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); - readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; - - private _onDidChangeSelection: Emitter = this._register(new Emitter()); - readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; - - private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); - readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; - - private readonly _onDidChangeActions: Emitter = this._register(new Emitter()); - readonly onDidChangeActions: Event = this._onDidChangeActions.event; - - private readonly _onDidChangeWelcomeState: Emitter = this._register(new Emitter()); - readonly onDidChangeWelcomeState: Event = this._onDidChangeWelcomeState.event; - - private readonly _onDidChangeTitle: Emitter = this._register(new Emitter()); - readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; - - private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); - - constructor( - readonly id: string, - private _title: string, - @IThemeService private readonly themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ICommandService private readonly commandService: ICommandService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IProgressService protected readonly progressService: IProgressService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @INotificationService private readonly notificationService: INotificationService, - @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - super(); - this.root = new Root(); - this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, false); - this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); - this.refreshContextKey = new RawContextKey(`treeView.${this.id}.enableRefresh`, false); - this.refreshContext = this.refreshContextKey.bindTo(contextKeyService); - - this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); - this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('explorer.decorations')) { - this.doRefresh([this.root]); /** soft refresh **/ - } - })); - this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - if (views.some(v => v.id === this.id)) { - this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); - } - })); - this.registerActions(); - - this.create(); - } - - get viewContainer(): ViewContainer { - return this.viewDescriptorService.getViewContainerByViewId(this.id)!; - } - - get viewLocation(): ViewContainerLocation { - return this.viewDescriptorService.getViewLocationById(this.id)!; - } - - private _dataProvider: ITreeViewDataProvider | undefined; - get dataProvider(): ITreeViewDataProvider | undefined { - return this._dataProvider; - } - - set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { - if (this.tree === undefined) { - this.createTree(); - } - - if (dataProvider) { - this._dataProvider = new class implements ITreeViewDataProvider { - private _isEmpty: boolean = true; - private _onDidChangeEmpty: Emitter = new Emitter(); - public onDidChangeEmpty: Event = this._onDidChangeEmpty.event; - - get isTreeEmpty(): boolean { - return this._isEmpty; - } - - async getChildren(node: ITreeItem): Promise { - let children: ITreeItem[]; - if (node && node.children) { - children = node.children; - } else { - children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); - node.children = children; - } - if (node instanceof Root) { - const oldEmpty = this._isEmpty; - this._isEmpty = children.length === 0; - if (oldEmpty !== this._isEmpty) { - this._onDidChangeEmpty.fire(); - } - } - return children; - } - }; - if (this._dataProvider.onDidChangeEmpty) { - this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); - } - this.updateMessage(); - this.refresh(); - } else { - this._dataProvider = undefined; - this.updateMessage(); - } - - this._onDidChangeWelcomeState.fire(); - } - - private _message: string | undefined; - get message(): string | undefined { - return this._message; - } - - set message(message: string | undefined) { - this._message = message; - this.updateMessage(); - this._onDidChangeWelcomeState.fire(); - } - - get title(): string { - return this._title; - } - - set title(name: string) { - this._title = name; - this._onDidChangeTitle.fire(this._title); - } - - get canSelectMany(): boolean { - return this._canSelectMany; - } - - set canSelectMany(canSelectMany: boolean) { - this._canSelectMany = canSelectMany; - } - - get hasIconForParentNode(): boolean { - return this._hasIconForParentNode; - } - - get hasIconForLeafNode(): boolean { - return this._hasIconForLeafNode; - } - - get visible(): boolean { - return this.isVisible; - } - - get showCollapseAllAction(): boolean { - return !!this.collapseAllContext.get(); - } - - set showCollapseAllAction(showCollapseAllAction: boolean) { - this.collapseAllContext.set(showCollapseAllAction); - } - - get showRefreshAction(): boolean { - return !!this.refreshContext.get(); - } - - set showRefreshAction(showRefreshAction: boolean) { - this.refreshContext.set(showRefreshAction); - } - - private registerActions() { - const that = this; - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.refresh`, - title: localize('refresh', "Refresh"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey), - group: 'navigation', - order: Number.MAX_SAFE_INTEGER - 1, - }, - icon: { id: 'codicon/refresh' } - }); - } - async run(): Promise { - return that.refresh(); - } - })); - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.collapseAll`, - title: localize('collapseAll', "Collapse All"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey), - group: 'navigation', - order: Number.MAX_SAFE_INTEGER, - }, - icon: { id: 'codicon/collapse-all' } - }); - } - async run(): Promise { - if (that.tree) { - return new CollapseAllAction(that.tree, true).run(); - } - } - })); - } - - setVisibility(isVisible: boolean): void { - isVisible = !!isVisible; - if (this.isVisible === isVisible) { - return; - } - - this.isVisible = isVisible; - - if (this.tree) { - if (this.isVisible) { - DOM.show(this.tree.getHTMLElement()); - } else { - DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it - } - - if (this.isVisible && this.elementsToRefresh.length) { - this.doRefresh(this.elementsToRefresh); - this.elementsToRefresh = []; - } - } - - this._onDidChangeVisibility.fire(this.isVisible); - } - - focus(reveal: boolean = true): void { - if (this.tree && this.root.children && this.root.children.length > 0) { - // Make sure the current selected element is revealed - const selectedElement = this.tree.getSelection()[0]; - if (selectedElement && reveal) { - this.tree.reveal(selectedElement, 0.5); - } - - // Pass Focus to Viewer - this.tree.domFocus(); - } else if (this.tree) { - this.tree.domFocus(); - } else { - this.domNode.focus(); - } - } - - show(container: HTMLElement): void { - DOM.append(container, this.domNode); - } - - private create() { - this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); - this.messageElement = DOM.append(this.domNode, DOM.$('.message')); - this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); - DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); - DOM.addClass(this.treeContainer, 'show-file-icons'); - const focusTracker = this._register(DOM.trackFocus(this.domNode)); - this._register(focusTracker.onDidFocus(() => this.focused = true)); - this._register(focusTracker.onDidBlur(() => this.focused = false)); - } - - private createTree() { - const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; - const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); - this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); - const aligner = new Aligner(this.themeService); - const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); - const widgetAriaLabel = this._title; - - this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer], - dataSource, { - identityProvider: new TreeViewIdentityProvider(), - accessibilityProvider: { - getAriaLabel(element: ITreeItem): string { - if (element.accessibilityInformation) { - return element.accessibilityInformation.label; - } - - return element.tooltip ? element.tooltip : element.label ? element.label.label : ''; - }, - getRole(element: ITreeItem): string | undefined { - return element.accessibilityInformation?.role ?? 'treeitem'; - }, - getWidgetAriaLabel(): string { - return widgetAriaLabel; - } - }, - keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: (item: ITreeItem) => { - return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); - } - }, - expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command, - collapseByDefault: (e: ITreeItem): boolean => { - return e.collapsibleState !== TreeItemCollapsibleState.Expanded; - }, - multipleSelectionSupport: this.canSelectMany, - overrideStyles: { - listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND - } - }) as WorkbenchAsyncDataTree); - aligner.tree = this.tree; - const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()); - renderer.actionRunner = actionRunner; - - this.tree.contextKeyService.createKey(this.id, true); - this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); - this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); - this._register(this.tree.onDidChangeCollapseState(e => { - if (!e.node.element) { - return; - } - - const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; - if (e.node.collapsed) { - this._onDidCollapseItem.fire(element); - } else { - this._onDidExpandItem.fire(element); - } - })); - this.tree.setInput(this.root).then(() => this.updateContentAreas()); - - this._register(this.tree.onDidOpen(e => { - if (!e.browserEvent) { - return; - } - const selection = this.tree!.getSelection(); - if ((selection.length === 1) && selection[0].command) { - this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || [])); - } - })); - } - - private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { - const node: ITreeItem | null = treeEvent.element; - if (node === null) { - return; - } - const event: UIEvent = treeEvent.browserEvent; - - event.preventDefault(); - event.stopPropagation(); - - this.tree!.setFocus([node]); - const actions = treeMenus.getResourceContextActions(node); - if (!actions.length) { - return; - } - this.contextMenuService.showContextMenu({ - getAnchor: () => treeEvent.anchor, - - getActions: () => actions, - - getActionViewItem: (action) => { - const keybinding = this.keybindingService.lookupKeybinding(action.id); - if (keybinding) { - return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); - } - return undefined; - }, - - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - this.tree!.domFocus(); - } - }, - - getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), - - actionRunner - }); - } - - protected updateMessage(): void { - if (this._message) { - this.showMessage(this._message); - } else if (!this.dataProvider) { - this.showMessage(noDataProviderMessage); - } else { - this.hideMessage(); - } - this.updateContentAreas(); - } - - private showMessage(message: string): void { - DOM.removeClass(this.messageElement, 'hide'); - this.resetMessageElement(); - this._messageValue = message; - if (!isFalsyOrWhitespace(this._message)) { - this.messageElement.textContent = this._messageValue; - } - this.layout(this._height, this._width); - } - - private hideMessage(): void { - this.resetMessageElement(); - DOM.addClass(this.messageElement, 'hide'); - this.layout(this._height, this._width); - } - - private resetMessageElement(): void { - DOM.clearNode(this.messageElement); - } - - private _height: number = 0; - private _width: number = 0; - layout(height: number, width: number) { - if (height && width) { - this._height = height; - this._width = width; - const treeHeight = height - DOM.getTotalHeight(this.messageElement); - this.treeContainer.style.height = treeHeight + 'px'; - if (this.tree) { - this.tree.layout(treeHeight, width); - } - } - } - - getOptimalWidth(): number { - if (this.tree) { - const parentNode = this.tree.getHTMLElement(); - const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); - return DOM.getLargestChildWidth(parentNode, childNodes); - } - return 0; - } - - async refresh(elements?: ITreeItem[]): Promise { - if (this.dataProvider && this.tree) { - if (this.refreshing) { - await Event.toPromise(this._onDidCompleteRefresh.event); - } - if (!elements) { - elements = [this.root]; - // remove all waiting elements to refresh if root is asked to refresh - this.elementsToRefresh = []; - } - for (const element of elements) { - element.children = undefined; // reset children - } - if (this.isVisible) { - return this.doRefresh(elements); - } else { - if (this.elementsToRefresh.length) { - const seen: Set = new Set(); - this.elementsToRefresh.forEach(element => seen.add(element.handle)); - for (const element of elements) { - if (!seen.has(element.handle)) { - this.elementsToRefresh.push(element); - } - } - } else { - this.elementsToRefresh.push(...elements); - } - } - } - return undefined; - } - - async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { - const tree = this.tree; - if (tree) { - itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - await Promise.all(itemOrItems.map(element => { - return tree.expand(element, false); - })); - } - } - - setSelection(items: ITreeItem[]): void { - if (this.tree) { - this.tree.setSelection(items); - } - } - - setFocus(item: ITreeItem): void { - if (this.tree) { - this.focus(); - this.tree.setFocus([item]); - } - } - - async reveal(item: ITreeItem): Promise { - if (this.tree) { - return this.tree.reveal(item); - } - } - - private refreshing: boolean = false; - private async doRefresh(elements: ITreeItem[]): Promise { - const tree = this.tree; - if (tree && this.visible) { - this.refreshing = true; - await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); - this.refreshing = false; - this._onDidCompleteRefresh.fire(); - this.updateContentAreas(); - if (this.focused) { - this.focus(false); - } - } - } - - private updateContentAreas(): void { - const isTreeEmpty = !this.root.children || this.root.children.length === 0; - // Hide tree container only when there is a message and tree is empty and not refreshing - if (this._messageValue && isTreeEmpty && !this.refreshing) { - DOM.addClass(this.treeContainer, 'hide'); - this.domNode.setAttribute('tabindex', '0'); - } else { - DOM.removeClass(this.treeContainer, 'hide'); - this.domNode.removeAttribute('tabindex'); - } - } -} - -class TreeViewIdentityProvider implements IIdentityProvider { - getId(element: ITreeItem): { toString(): string; } { - return element.handle; - } -} - -class TreeViewDelegate implements IListVirtualDelegate { - - getHeight(element: ITreeItem): number { - return TreeRenderer.ITEM_HEIGHT; - } - - getTemplateId(element: ITreeItem): string { - return TreeRenderer.TREE_TEMPLATE_ID; - } -} - -class TreeDataSource implements IAsyncDataSource { - - constructor( - private treeView: ITreeView, - private withProgress: (task: Promise) => Promise - ) { - } - - hasChildren(element: ITreeItem): boolean { - return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None); - } - - async getChildren(element: ITreeItem): Promise { - if (this.treeView.dataProvider) { - return this.withProgress(this.treeView.dataProvider.getChildren(element)); - } - return []; - } -} - -// todo@joh,sandy make this proper and contributable from extensions -registerThemingParticipant((theme, collector) => { - - const matchBackgroundColor = theme.getColor(listFilterMatchHighlight); - if (matchBackgroundColor) { - collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); - collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); - } - const matchBorderColor = theme.getColor(listFilterMatchHighlightBorder); - if (matchBorderColor) { - collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); - collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); - } - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); - } - const focusBorderColor = theme.getColor(focusBorder); - if (focusBorderColor) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`); - } - const codeBackground = theme.getColor(textCodeBlockBackground); - if (codeBackground) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`); - } -}); - -interface ITreeExplorerTemplateData { - elementDisposable: IDisposable; - container: HTMLElement; - resourceLabel: IResourceLabel; - icon: HTMLElement; - actionBar: ActionBar; -} - -class TreeRenderer extends Disposable implements ITreeRenderer { - static readonly ITEM_HEIGHT = 22; - static readonly TREE_TEMPLATE_ID = 'treeExplorer'; - - private _actionRunner: MultipleSelectionActionRunner | undefined; - - constructor( - private treeViewId: string, - private menus: TreeMenus, - private labels: ResourceLabels, - private actionViewItemProvider: IActionViewItemProvider, - private aligner: Aligner, - @IThemeService private readonly themeService: IThemeService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ILabelService private readonly labelService: ILabelService - ) { - super(); - } - - get templateId(): string { - return TreeRenderer.TREE_TEMPLATE_ID; - } - - set actionRunner(actionRunner: MultipleSelectionActionRunner) { - this._actionRunner = actionRunner; - } - - renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { - DOM.addClass(container, 'custom-view-tree-node-item'); - - const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); - - const resourceLabel = this.labels.create(container, { supportHighlights: true }); - const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); - const actionBar = new ActionBar(actionsContainer, { - actionViewItemProvider: this.actionViewItemProvider - }); - - return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None }; - } - - renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { - templateData.elementDisposable.dispose(); - const node = element.element; - const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; - const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : resource ? { label: basename(resource) } : undefined; - const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined; - const label = treeItemLabel ? treeItemLabel.label : undefined; - const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => { - if (start < 0) { - start = label.length + start; - } - if (end < 0) { - end = label.length + end; - } - if ((start >= label.length) || (end > label.length)) { - return ({ start: 0, end: 0 }); - } - if (start > end) { - const swap = start; - start = end; - end = swap; - } - return ({ start, end }); - }) : undefined; - const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark; - const iconUrl = icon ? URI.revive(icon) : null; - const title = node.tooltip ? node.tooltip : resource ? undefined : label; - - // reset - templateData.actionBar.clear(); - - if (resource || this.isFileKindThemeIcon(node.themeIcon)) { - const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); - templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) }); - } else { - templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) }); - } - - templateData.icon.title = title ? title : ''; - - if (iconUrl) { - templateData.icon.className = 'custom-view-tree-node-item-icon'; - templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); - - } else { - let iconClass: string | undefined; - if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) { - iconClass = ThemeIcon.asClassName(node.themeIcon); - } - templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : ''; - templateData.icon.style.backgroundImage = ''; - } - - templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; - templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); - if (this._actionRunner) { - templateData.actionBar.actionRunner = this._actionRunner; - } - this.setAlignment(templateData.container, node); - templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); - } - - private setAlignment(container: HTMLElement, treeItem: ITreeItem) { - DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); - } - - private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { - if (icon) { - return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id; - } else { - return false; - } - } - - private getFileKind(node: ITreeItem): FileKind { - if (node.themeIcon) { - switch (node.themeIcon.id) { - case FileThemeIcon.id: - return FileKind.FILE; - case FolderThemeIcon.id: - return FileKind.FOLDER; - } - } - return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; - } - - disposeElement(resource: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { - templateData.elementDisposable.dispose(); - } - - disposeTemplate(templateData: ITreeExplorerTemplateData): void { - templateData.resourceLabel.dispose(); - templateData.actionBar.dispose(); - templateData.elementDisposable.dispose(); - } -} - -class Aligner extends Disposable { - private _tree: WorkbenchAsyncDataTree | undefined; - - constructor(private themeService: IThemeService) { - super(); - } - - set tree(tree: WorkbenchAsyncDataTree) { - this._tree = tree; - } - - public alignIconWithTwisty(treeItem: ITreeItem): boolean { - if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) { - return false; - } - if (!this.hasIcon(treeItem)) { - return false; - } - - if (this._tree) { - const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); - if (this.hasIcon(parent)) { - return false; - } - return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); - } else { - return false; - } - } - - private hasIcon(node: ITreeItem): boolean { - const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark; - if (icon) { - return true; - } - if (node.resourceUri || node.themeIcon) { - const fileIconTheme = this.themeService.getFileIconTheme(); - const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None; - if (isFolder) { - return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons; - } - return fileIconTheme.hasFileIcons; - } - return false; - } -} - -class MultipleSelectionActionRunner extends ActionRunner { - - constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) { - super(); - this._register(this.onDidRun(e => { - if (e.error) { - notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id)); - } - })); - } - - runAction(action: IAction, context: TreeViewItemHandleArg): Promise { - const selection = this.getSelectedResources(); - let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; - let actionInSelected: boolean = false; - if (selection.length > 1) { - selectionHandleArgs = selection.map(selected => { - if (selected.handle === context.$treeItemHandle) { - actionInSelected = true; - } - return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle }; - }); - } - - if (!actionInSelected) { - selectionHandleArgs = undefined; - } - - return action.run(...[context, selectionHandleArgs]); - } -} - -class TreeMenus extends Disposable implements IDisposable { - - constructor( - private id: string, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService - ) { - super(); - } - - getResourceActions(element: ITreeItem): IAction[] { - return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary; - } - - getResourceContextActions(element: ITreeItem): IAction[] { - return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary; - } - - private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } { - const contextKeyService = this.contextKeyService.createScoped(); - contextKeyService.createKey('view', this.id); - contextKeyService.createKey(context.key, context.value); - - const menu = this.menuService.createMenu(menuId, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); - - menu.dispose(); - contextKeyService.dispose(); - - return result; - } -} - -export class CustomTreeView extends TreeView { - - private activated: boolean = false; - - constructor( - id: string, - title: string, - @IThemeService themeService: IThemeService, - @IInstantiationService instantiationService: IInstantiationService, - @ICommandService commandService: ICommandService, - @IConfigurationService configurationService: IConfigurationService, - @IProgressService progressService: IProgressService, - @IContextMenuService contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService notificationService: INotificationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IExtensionService private readonly extensionService: IExtensionService, - ) { - super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService); - } - - setVisibility(isVisible: boolean): void { - super.setVisibility(isVisible); - if (this.visible) { - this.activate(); - } - } - - private activate() { - if (!this.activated) { - this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) - .then(() => timeout(2000)) - .then(() => { - this.updateMessage(); - }); - this.activated = true; - } - } -} diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 5f3fc0fd9bf..4c94254d513 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -450,7 +450,7 @@ export abstract class ViewPane extends Pane implements IView { private setActions(): void { if (this.toolbar) { - this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions()))(); + this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); this.toolbar.context = this.getActionsContext(); } } @@ -1040,14 +1040,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - const viewToggleActions = this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1), - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - + const viewToggleActions = this.getViewsVisibilityActions(); if (result.length && viewToggleActions.length) { result.push(new Separator()); } @@ -1073,6 +1066,16 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return []; } + getViewsVisibilityActions(): IAction[] { + return this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ + id: `${viewDescriptor.id}.toggleVisibility`, + label: viewDescriptor.name, + checked: this.viewContainerModel.isVisible(viewDescriptor.id), + enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1), + run: () => this.toggleViewVisibility(viewDescriptor.id) + })); + } + getActionViewItem(action: IAction): IActionViewItem | undefined { if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getActionViewItem(action); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index 7deee848574..1086f10aa19 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/views'; import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IViewDescriptorService, ViewContainer, IViewDescriptor, IView, ViewContainerLocation, IViewsService, IViewPaneContainer, getVisbileViewContextKey } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 9c4d5a10f01..91431c7e3de 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -27,6 +27,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ContextSubMenu } from 'vs/base/browser/contextmenu'; +import { Event } from 'vs/base/common/event'; export abstract class Viewlet extends PaneComposite implements IViewlet { @@ -43,6 +45,10 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { @IConfigurationService protected configurationService: IConfigurationService ) { super(id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this._register(Event.any(viewPaneContainer.onDidAddViews, viewPaneContainer.onDidRemoveViews)(() => { + // Update title area since there is no better way to update secondary actions + this.updateTitleArea(); + })); } getContextMenuActions(): IAction[] { @@ -60,6 +66,24 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { run: () => this.layoutService.setSideBarHidden(true) }]; } + + getSecondaryActions(): IAction[] { + const viewSecondaryActions = this.viewPaneContainer.getViewsVisibilityActions(); + const secondaryActions = this.viewPaneContainer.getSecondaryActions(); + if (viewSecondaryActions.length <= 1) { + return secondaryActions; + } + + if (secondaryActions.length === 0) { + return viewSecondaryActions; + } + + return [ + new ContextSubMenu(nls.localize('views', "Views"), viewSecondaryActions), + new Separator(), + ...secondaryActions + ]; + } } /** diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 36f3c13126f..2ccb10ae224 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -23,6 +23,7 @@ import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import Severity from 'vs/base/common/severity'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -580,6 +581,7 @@ export interface ITreeView extends IDisposable { setFocus(item: ITreeItem): void; + show(container: any): void; } export interface IRevealOptions { @@ -635,7 +637,7 @@ export interface ITreeItem { resourceUri?: UriComponents; - tooltip?: string; + tooltip?: string | IMarkdownString; contextValue?: string; diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts index 76758b30160..c1bc2e5377d 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -9,7 +9,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { SymbolKinds, Location } from 'vs/editor/common/modes'; +import { SymbolKinds, Location, SymbolTag } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -117,11 +117,12 @@ export class CallRenderer implements ITreeRenderer, _index: number, template: CallRenderingTemplate): void { const { element, filterData } = node; + const deprecated = element.item.tags?.includes(SymbolTag.Deprecated); template.icon.className = SymbolKinds.toCssClassName(element.item.kind, true); template.label.setLabel( element.item.name, element.item.detail, - { labelEscapeNewLines: true, matches: createMatches(filterData) } + { labelEscapeNewLines: true, matches: createMatches(filterData), strikethrough: deprecated } ); } disposeTemplate(template: CallRenderingTemplate): void { diff --git a/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts b/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts index c82d0a7739d..28a29548511 100644 --- a/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts +++ b/src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IRange } from 'vs/editor/common/core/range'; -import { SymbolKind, ProviderResult } from 'vs/editor/common/modes'; +import { SymbolKind, ProviderResult, SymbolTag } from 'vs/editor/common/modes'; import { ITextModel } from 'vs/editor/common/model'; import { CancellationToken } from 'vs/base/common/cancellation'; import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry'; @@ -32,6 +32,7 @@ export interface CallHierarchyItem { uri: URI; range: IRange; selectionRange: IRange; + tags?: SymbolTag[] } export interface IncomingCall { diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 02867d26c03..544fe7d145f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -197,7 +197,7 @@ export class CommentNode extends Disposable { this.createToolbar(); } - this.toolbar!.setActions(primary, secondary)(); + this.toolbar!.setActions(primary, secondary); })); const { primary, secondary } = this.getToolbarActions(menu); @@ -205,7 +205,7 @@ export class CommentNode extends Disposable { if (actions.length || secondary.length) { this.createToolbar(); - this.toolbar!.setActions(actions, secondary)(); + this.toolbar!.setActions(actions, secondary); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index afec917d62d..fc384e8a262 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Registry } from 'vs/platform/registry/common/platform'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; @@ -11,6 +10,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { basename, extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -18,17 +18,18 @@ import { EditorActivation, IEditorOptions, ITextEditorOptions } from 'vs/platfor import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { EditorInput, EditorOptions, GroupIdentifier, IEditorInput, IEditorPane, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, Extensions as EditorInputExtensions, GroupIdentifier, IEditorInput, IEditorInputFactoryRegistry, IEditorPane } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; -import { CustomEditorAssociation, CustomEditorsAssociations, customEditorsAssociationsSettingId, defaultEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorOpenWith'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { CustomEditorAssociation, CustomEditorsAssociations, customEditorsAssociationsSettingId, defaultEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorOpenWith'; import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService, IOpenEditorOverride, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; import { ContributedCustomEditors, defaultCustomEditor } from '../common/contributedCustomEditors'; import { CustomEditorInput } from './customEditorInput'; @@ -80,6 +81,14 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } })); + const PRIORITY = 105; + this._register(UndoCommand.addImplementation(PRIORITY, () => { + return this.withActiveCustomEditor(editor => editor.undo()); + })); + this._register(RedoCommand.addImplementation(PRIORITY, () => { + return this.withActiveCustomEditor(editor => editor.redo()); + })); + this.updateContexts(); } @@ -87,6 +96,15 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return [...this._contributedEditors]; } + private withActiveCustomEditor(f: (editor: CustomEditorInput) => void): boolean { + const activeEditor = this.editorService.activeEditor; + if (activeEditor instanceof CustomEditorInput) { + f(activeEditor); + return true; + } + return false; + } + public get models() { return this._models; } public getCustomEditor(viewType: string): CustomEditorInfo | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index fdac8b72db1..c4369cd44e7 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -501,23 +501,23 @@ class SessionsRenderer implements ICompressibleTreeRenderer, _: number, data: ISessionTemplateData): void { - this.doRenderElement(element.element, element.element.getLabel(), createMatches(element.filterData), data); + this.doRenderElement(element.element, element.element, createMatches(element.filterData), data); } renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: ISessionTemplateData, height: number | undefined): void { const lastElement = node.element.elements[node.element.elements.length - 1]; const matches = createMatches(node.filterData); - const label = node.element.elements[0].getLabel(); - this.doRenderElement(lastElement, label, matches, templateData); + const first = node.element.elements[0]; + this.doRenderElement(lastElement, first, matches, templateData); } - private doRenderElement(session: IDebugSession, label: string, matches: IMatch[], data: ISessionTemplateData): void { + private doRenderElement(session: IDebugSession, first: IDebugSession, matches: IMatch[], data: ISessionTemplateData): void { data.session.title = nls.localize({ key: 'session', comment: ['Session is a noun'] }, "Session"); - data.label.set(label, matches); + data.label.set(first.getLabel(), matches); const thread = session.getAllThreads().find(t => t.stopped); const setActionBar = () => { - const actions = getActions(this.instantiationService, session); + const actions = getActions(this.instantiationService, session, first); const primary: IAction[] = actions; const secondary: IAction[] = []; @@ -940,7 +940,7 @@ class CallStackAccessibilityProvider implements IListAccessibilityProvider { return [ thread.stopped ? instantiationService.createInstance(ContinueAction, thread) : instantiationService.createInstance(PauseAction, thread), @@ -955,7 +955,9 @@ function getActions(instantiationService: IInstantiationService, element: IDebug } const session = element; - const stopOrDisconectAction = isSessionAttach(session) ? instantiationService.createInstance(DisconnectAction, session) : instantiationService.createInstance(StopAction, session); + let sessionOrParent = sessionForStopOrDisconnect || session; + // Use the parent session for disconnect and stop so the compressed element stop would effect all elements #99736 + const stopOrDisconectAction = isSessionAttach(sessionOrParent) ? instantiationService.createInstance(DisconnectAction, sessionOrParent) : instantiationService.createInstance(StopAction, sessionOrParent); const restartAction = instantiationService.createInstance(RestartAction, session); const threads = session.getAllThreads(); if (threads.length === 1) { diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index b70398c1eb7..96bab8f09ee 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -310,7 +310,7 @@ class SessionTreeItem extends BaseTreeItem { return 999; } - addPath(source: Source): void { + async addPath(source: Source): Promise { let folder: IWorkspaceFolder | null; let url: string; @@ -347,9 +347,8 @@ class SessionTreeItem extends BaseTreeItem { } else { // on unix try to tildify absolute paths path = normalize(path); - const userHome = this._pathService.resolvedUserHome; - if (userHome && !isWindows) { - path = tildify(path, userHome.fsPath); + if (!isWindows) { + path = tildify(path, (await this._pathService.userHome()).fsPath); } } } @@ -518,27 +517,28 @@ export class LoadedScriptsView extends ViewPane { } }; - const addSourcePathsToSession = (session: IDebugSession) => { + const addSourcePathsToSession = async (session: IDebugSession) => { const sessionNode = root.add(session); - return session.getLoadedSources().then(paths => { - paths.forEach(path => sessionNode.addPath(path)); - scheduleRefreshOnVisible(); - }); + const paths = await session.getLoadedSources(); + for (const path of paths) { + await sessionNode.addPath(path); + } + scheduleRefreshOnVisible(); }; const registerSessionListeners = (session: IDebugSession) => { - this._register(session.onDidChangeName(() => { + this._register(session.onDidChangeName(async () => { // Re-add session, this will trigger proper sorting and id recalculation. root.remove(session.getId()); - addSourcePathsToSession(session); + await addSourcePathsToSession(session); })); - this._register(session.onDidLoadedSource(event => { + this._register(session.onDidLoadedSource(async event => { let sessionRoot: SessionTreeItem; switch (event.reason) { case 'new': case 'changed': sessionRoot = root.add(session); - sessionRoot.addPath(event.source); + await sessionRoot.addPath(event.source); scheduleRefreshOnVisible(); if (event.reason === 'changed') { DebugContentProvider.refreshDebugContent(event.source.uri); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index a54c2a498f1..495fdde4c9e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -194,6 +194,11 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'array', description: localize('handleUriConfirmedExtensions', "When an extension is listed here, a confirmation prompt will not be shown when that extension handles a URI."), default: [] + }, + 'extensions.webWorker': { + type: 'boolean', + description: localize('extensionsWebWorker', "Enable web worker extension host."), + default: false } } }); diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts index c4b310c504a..84e406323be 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts @@ -40,7 +40,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ terminalService.openTerminal(paths.dirname(activeFile.fsPath)); } else { const pathService = accessor.get(IPathService); - const userHome = await pathService.userHome; + const userHome = await pathService.userHome(); terminalService.openTerminal(userHome.fsPath); } } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 4ea9ac2173a..485601fdb5d 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -271,7 +271,7 @@ export class OpenEditorsView extends ViewPane { })); const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); this._register(resourceNavigator.onDidOpen(e => { - if (!e.element) { + if (typeof e.element !== 'number') { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index bceac71a323..66b3a8d7d97 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -10,13 +10,15 @@ export const SCROLLABLE_ELEMENT_PADDING_TOP = 20; // Cell sizing related export const CELL_MARGIN = 20; export const CELL_RUN_GUTTER = 32; +export const CODE_CELL_LEFT_MARGIN = 45; export const EDITOR_TOOLBAR_HEIGHT = 0; export const BOTTOM_CELL_TOOLBAR_HEIGHT = 32; export const CELL_STATUSBAR_HEIGHT = 22; -// Top margin of editor -export const EDITOR_TOP_MARGIN = 0; +// Margin above editor +export const EDITOR_TOP_MARGIN = 8; +export const CELL_BOTTOM_MARGIN = 8; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 12; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index af4c1d1a694..ee1f9b3fd30 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/notebookFind'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -24,6 +25,10 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +const FIND_HIDE_TRANSITION = 'find-hide-transition'; +const FIND_SHOW_TRANSITION = 'find-show-transition'; + + export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; protected _findWidgetFocused: IContextKey; @@ -32,6 +37,8 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _currentMatch: number = -1; private _allMatchesDecorations: ICellModelDecorations[] = []; private _currentMatchDecorations: ICellModelDecorations[] = []; + private _showTimeout: number | null = null; + private _hideTimeout: number | null = null; constructor( private readonly _notebookEditor: INotebookEditor, @@ -136,11 +143,6 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote this._notebookEditor.revealRangeInCenterIfOutsideViewportAsync(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); } - hide() { - super.hide(); - this.set([]); - } - protected findFirst(): void { } protected onFocusTrackerFocus() { @@ -243,10 +245,56 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote }); } + show(initialInput?: string): void { + super.show(initialInput); + + if (this._showTimeout === null) { + if (this._hideTimeout !== null) { + window.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._notebookEditor.removeClassName(FIND_HIDE_TRANSITION); + } + + this._notebookEditor.addClassName(FIND_SHOW_TRANSITION); + this._showTimeout = window.setTimeout(() => { + this._notebookEditor.removeClassName(FIND_SHOW_TRANSITION); + this._showTimeout = null; + }, 200); + } else { + // no op + } + } + + hide() { + super.hide(); + this.set([]); + + if (this._hideTimeout === null) { + if (this._showTimeout !== null) { + window.clearTimeout(this._showTimeout); + this._showTimeout = null; + this._notebookEditor.removeClassName(FIND_SHOW_TRANSITION); + } + this._notebookEditor.addClassName(FIND_HIDE_TRANSITION); + this._hideTimeout = window.setTimeout(() => { + this._notebookEditor.removeClassName(FIND_HIDE_TRANSITION); + }, 200); + } else { + // no op + } + } + clear() { this._currentMatch = -1; this._findMatches = []; } + + dispose() { + this._notebookEditor?.removeClassName(FIND_SHOW_TRANSITION); + this._notebookEditor?.removeClassName(FIND_HIDE_TRANSITION); + super.dispose(); + } + } registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css b/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css new file mode 100644 index 00000000000..98fa75e3930 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .notebookOverlay.notebook-editor.find-hide-transition { + overflow-y: hidden; +} + +.monaco-workbench .notebookOverlay.notebook-editor.find-show-transition { + overflow-y: hidden; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 0e974e50106..729b2065290 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -58,11 +58,11 @@ padding-top: 8px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { - top: 8px !important; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-focus-indicator { + display: none; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell-focus-indicator { bottom: 8px; } @@ -202,7 +202,6 @@ outline: none !important; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; } @@ -218,7 +217,7 @@ position: absolute; height: 26px; right: 32px; - top: -20px; + top: -12px; /* this lines up the bottom toolbar border with the current line when on line 01 */ z-index: 30; } @@ -371,39 +370,56 @@ outline: none !important; } +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container { + height: 10px; + left: 0px; + right: 0px; + overflow: hidden; + position: absolute; +} -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-top { + top: -10px; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-shadow-top { + margin-top: 10px; + width: 100%; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator { display: block; content: ' '; position: absolute; - width: 32px; box-sizing: border-box; - border-left-width: 2px; - border-left-style: solid; - top: 22px; + top: 0px; visibility: hidden; - opacity: 0.6; + opacity: 1; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator:hover { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { + width: 100%; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right { + right: 0px; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator:hover { cursor: grab; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator .codicon:hover { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator .codicon:hover { cursor: pointer; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row:hover .cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator { visibility: visible; } -.monaco-workbench .notebookOverlay .monaco-list:focus .monaco-list-row .notebook-cell-focus-indicator, -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row .notebook-cell-focus-indicator { - opacity: 1; -} - .monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { z-index: 20; content: ""; @@ -427,7 +443,6 @@ left: 0px; right: 0px; opacity: 0; - /* transition: opacity 0.2s ease-in-out; */ z-index: 10; } @@ -648,7 +663,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { visibility: visible; padding: 4px; - width: calc(100% - 8px); + width: calc(100% - 30px); } /** Theming */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index ba5c8e903bd..f4579397c17 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -270,7 +270,22 @@ export interface INotebookEditor extends IEditor { /** * Send message to the webview for outputs. */ - postMessage(message: any): void; + postMessage(forRendererId: string | undefined, message: any): void; + + /** + * Toggle class name on the notebook editor root DOM node. + */ + toggleClassName(className: string): void; + + /** + * Remove class name on the notebook editor root DOM node. + */ + addClassName(className: string): void; + + /** + * Remove class name on the notebook editor root DOM node. + */ + removeClassName(className: string): void; /** * Trigger the editor to scroll from scroll event programmatically @@ -440,6 +455,12 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { editor: ICodeEditor; progressBar: ProgressBar; timer: TimerRenderer; + focusIndicatorRight: HTMLElement; + focusIndicatorBottom: HTMLElement; +} + +export function isCodeCellRenderTemplate(templateData: BaseCellRenderTemplate): templateData is CodeCellRenderTemplate { + return !!(templateData as CodeCellRenderTemplate).runToolbar; } export interface IOutputTransformContribution { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index d304dd048b2..e1ee381a996 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -27,7 +27,7 @@ import { contrastBorder, editorBackground, focusBorder, foreground, registerColo import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; -import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; @@ -301,21 +301,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor transformOptimization: false, styleController: (_suffix: string) => { return this._list!; }, overrideStyles: { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, + listBackground: Color.transparent, + listActiveSelectionBackground: Color.transparent, listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionBackground: Color.transparent, listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, + listFocusBackground: Color.transparent, listFocusForeground: foreground, listHoverForeground: foreground, - listHoverBackground: editorBackground, + listHoverBackground: Color.transparent, listHoverOutline: focusBorder, listFocusOutline: focusBorder, - listInactiveSelectionBackground: editorBackground, + listInactiveSelectionBackground: Color.transparent, listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground, + listInactiveFocusBackground: Color.transparent, + listInactiveFocusOutline: Color.transparent, }, accessibilityProvider: { getAriaLabel() { return null; }, @@ -528,9 +528,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._onDidFocusEmitter.fire(); }); - this._localStore.add(this._webview.onMessage(message => { + this._localStore.add(this._webview.onMessage(({ message, forRenderer }) => { if (this.viewModel) { - this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), message); + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), forRenderer, message); } })); } @@ -1213,10 +1213,27 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._outputRenderer; } - postMessage(message: any) { - this._webview?.webview.postMessage(message); + postMessage(forRendererId: string | undefined, message: any) { + if (forRendererId === undefined) { + this._webview?.webview.postMessage(message); + } else { + this._webview?.postRendererMessage(forRendererId, message); + } } + toggleClassName(className: string) { + DOM.toggleClass(this._overlayContainer, className); + } + + addClassName(className: string) { + DOM.addClass(this._overlayContainer, className); + } + + removeClassName(className: string) { + DOM.removeClass(this._overlayContainer, className); + } + + //#endregion //#region Editor Contributions @@ -1260,12 +1277,6 @@ export const notebookCellBorder = registerColor('notebook.cellBorderColor', { hc: PANEL_BORDER }, nls.localize('notebook.cellBorderColor', "The border color for notebook cells.")); -export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { - light: focusBorder, - dark: focusBorder, - hc: focusBorder -}, nls.localize('notebook.focusedCellIndicator', "The color of the notebook cell indicator.")); - export const focusedEditorIndicator = registerColor('notebook.focusedEditorIndicator', { light: focusBorder, dark: focusBorder, @@ -1301,7 +1312,19 @@ export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperat dark: Color.fromHex('#808080').transparent(0.35), light: Color.fromHex('#808080').transparent(0.35), hc: contrastBorder -}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); +}, nls.localize('cellToolbarSeperator', "The color of the seperator in the cell bottom toolbar")); + +export const cellFocusBackground = registerColor('notebook.cellFocusBackground', { + dark: transparent(PANEL_BORDER, .6), + light: transparent(PANEL_BORDER, .4), + hc: PANEL_BORDER +}, nls.localize('cellFocusBackground', "The background color of focused or hovered cells")); + +export const focusedCellShadow = registerColor('notebook.focusedCellShadow', { + dark: Color.black.transparent(0.8), + light: Color.black.transparent(0.06), + hc: Color.transparent +}, nls.localize('cellShadow', "The color of the shadow on the focused or hovered cell")); export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), @@ -1309,6 +1332,13 @@ export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemH hc: new Color(new RGBA(255, 255, 255, 0.15)), }, nls.localize('notebook.cellStatusBarItemHoverBackground', "The background color of notebook cell status bar items.")); +export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', { + light: focusBorder, + dark: focusBorder, + hc: focusBorder +}, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); + + registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; @@ -1369,15 +1399,15 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .separator { background-color: ${cellToolbarSeperator} }`); collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item:first-child::after { background-color: ${cellToolbarSeperator} }`); collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); - collector.addRule(`.notebookOverlay .monaco-list-row:hover .notebook-cell-focus-indicator, - .notebookOverlay .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); } - const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); - if (focusedCellIndicatorColor) { - collector.addRule(`.notebookOverlay .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.notebookOverlay .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); + const cellFocusBackgroundColor = theme.getColor(cellFocusBackground); + if (cellFocusBackgroundColor) { + collector.addRule(`.notebookOverlay .code-cell-row:hover .cell-focus-indicator, + .notebookOverlay .code-cell-row.focused .cell-focus-indicator, + .notebookOverlay .code-cell-row.cell-output-hover .cell-focus-indicator { background-color: ${cellFocusBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .markdown-cell-row:hover, + .notebookOverlay .markdown-cell-row.focused { background-color: ${cellFocusBackgroundColor} !important; }`); } const focusedEditorIndicatorColor = theme.getColor(focusedEditorIndicator); @@ -1415,23 +1445,38 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: ${cellStatusBarHoverBg}; }`); } - // const widgetShadowColor = theme.getColor(widgetShadow); - // if (widgetShadowColor) { - // collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { - // box-shadow: 0 0 8px 4px ${widgetShadowColor} - // }`) - // } + const cellShadowColor = theme.getColor(focusedCellShadow); + if (cellShadowColor) { + // Code cells + collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-shadow { box-shadow: 0px 2px 8px 2px ${cellShadowColor} }`); + + // Markdown cells + collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row:hover, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused { box-shadow: 0px 2px 8px 2px ${cellShadowColor} }`); + } + + const cellInsertionIndicatorColor = theme.getColor(cellInsertionIndicator); + if (cellInsertionIndicatorColor) { + collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { background-color: ${cellInsertionIndicatorColor}; }`); + } // Cell Margin collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row { padding-bottom: ${CELL_BOTTOM_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row .cell-bottom-toolbar-container { margin-top: ${CELL_BOTTOM_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .markdown-cell-row .cell .cell-editor-part { margin-left: ${CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; bottom: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top { height: ${EDITOR_TOP_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${CELL_BOTTOM_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${CELL_BOTTOM_MARGIN}px; }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 9b0346cd050..1ba2932fe33 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -683,6 +683,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onNotebookEditorAdd.fire(editor); } + getNotebookEditor(editorId: string) { + return this._notebookEditors.get(editorId); + } + listNotebookEditors(): INotebookEditor[] { return [...this._notebookEditors].map(e => e[1]); } @@ -750,11 +754,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu return; } - onDidReceiveMessage(viewType: string, editorId: string, message: any): void { + onDidReceiveMessage(viewType: string, editorId: string, rendererType: string | undefined, message: any): void { let provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.onDidReceiveMessage(editorId, message); + return provider.controller.onDidReceiveMessage(editorId, rendererType, message); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index eb4a13b0da0..6ec55e38bdf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -529,7 +529,7 @@ export class NotebookCellList extends WorkbenchList implements ID // override domFocus() { const focused = this.getFocusedElements()[0]; - const focusedDomElement = this.domElementOfElement(focused); + const focusedDomElement = focused && this.domElementOfElement(focused); if (document.activeElement && focusedDomElement && focusedDomElement.contains(document.activeElement)) { // for example, when focus goes into monaco editor, if we refocus the list view, the editor will lose focus. diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 128bfdb6fc0..fafd0553874 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -76,7 +76,7 @@ export interface IBlurOutputMessage { } export interface IClickedDataUrlMessage { - __vscode_notebook_message: string; + __vscode_notebook_message: boolean; type: 'clicked-data-url'; data: string; downloadName?: string; @@ -156,6 +156,13 @@ export interface IUpdatePreloadResourceMessage { source: 'renderer' | 'kernel'; } +export interface ICustomRendererMessage { + __vscode_notebook_message: boolean; + type: 'customRendererMessage'; + rendererId: string; + message: unknown; +} + export type FromWebviewMessage = | WebviewIntialized | IDimensionMessage @@ -163,7 +170,9 @@ export type FromWebviewMessage = | IMouseLeaveMessage | IWheelMessage | IScrollAckMessage - | IBlurOutputMessage; + | IBlurOutputMessage + | ICustomRendererMessage + | IClickedDataUrlMessage; export type ToWebviewMessage = | IClearMessage @@ -175,7 +184,8 @@ export type ToWebviewMessage = | IHideOutputMessage | IShowOutputMessage | IUpdatePreloadResourceMessage - | IFocusOutputMessage; + | IFocusOutputMessage + | ICustomRendererMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; @@ -194,7 +204,10 @@ function html(strings: TemplateStringsArray, ...values: any[]): string { return str; } -type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage | WebviewIntialized | IClickedDataUrlMessage; +export interface INotebookWebviewMessage { + message: unknown; + forRenderer?: string; +} let version = 0; export class BackLayerWebView extends Disposable { @@ -207,8 +220,8 @@ export class BackLayerWebView extends Disposable { localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; kernelRootsCache: URI[] = []; - private readonly _onMessage = this._register(new Emitter()); - public readonly onMessage: Event = this._onMessage.event; + private readonly _onMessage = this._register(new Emitter()); + public readonly onMessage: Event = this._onMessage.event; private _loaded!: Promise; private _initalized?: Promise; private _disposed = false; @@ -267,6 +280,15 @@ export class BackLayerWebView extends Disposable { `; } + postRendererMessage(rendererId: string, message: any) { + this._sendMessageToWebview({ + __vscode_notebook_message: true, + type: 'customRendererMessage', + message, + rendererId + }); + } + private resolveOutputId(id: string): { cell: CodeCellViewModel, output: IProcessedOutput } | undefined { const output = this.reversedInsetMapping.get(id); if (!output) { @@ -341,7 +363,7 @@ ${loaderJs} } })); - this._register(this.webview.onMessage((data: IMessage) => { + this._register(this.webview.onMessage((data: FromWebviewMessage) => { if (data.__vscode_notebook_message) { if (data.type === 'dimension') { let height = data.data.height; @@ -397,11 +419,13 @@ ${loaderJs} } } else if (data.type === 'clicked-data-url') { this._onDidClickDataLink(data); + } else if (data.type === 'customRendererMessage') { + this._onMessage.fire({ message: data.message, forRenderer: data.rendererId }); } return; } - this._onMessage.fire(data); + this._onMessage.fire({ message: data }); })); } @@ -459,7 +483,7 @@ ${loaderJs} resolveFunc = resolve; }); - let dispose = webview.onMessage((data: IMessage) => { + let dispose = webview.onMessage((data: FromWebviewMessage) => { if (data.__vscode_notebook_message && data.type === 'initialized') { resolveFunc(); dispose.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index af4adb6343c..c112f0b7b5a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -38,9 +38,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BOTTOM_CELL_TOOLBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext, CELL_TITLE_GROUP_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, MarkdownCellRenderTemplate, isCodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; @@ -199,14 +199,13 @@ abstract class AbstractCellRenderer { }); toolbar.getContainer().style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}px`; - container.style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}px`; const cellMenu = this.instantiationService.createInstance(CellMenus); const menu = disposables.add(cellMenu.getCellInsertionMenu(contextKeyService)); const actions = this.getCellToolbarActions(menu); - toolbar.setActions(actions.primary, actions.secondary)(); + toolbar.setActions(actions.primary, actions.secondary); return toolbar; } @@ -223,9 +222,6 @@ abstract class AbstractCellRenderer { const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; container.style.top = `${bottomToolbarOffset}px`; })); - } else { - container.style.position = 'static'; - container.style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}`; } } @@ -267,7 +263,7 @@ abstract class AbstractCellRenderer { const actions = this.getCellToolbarActions(menu); const hadFocus = DOM.isAncestor(document.activeElement, templateData.toolbar.getContainer()); - templateData.toolbar.setActions(actions.primary, actions.secondary)(); + templateData.toolbar.setActions(actions.primary, actions.secondary); if (hadFocus) { this.notebookEditor.focus(); } @@ -276,9 +272,15 @@ abstract class AbstractCellRenderer { if (actions.primary.length || actions.secondary.length) { templateData.container.classList.add('cell-has-toolbar-actions'); templateData.focusIndicator.style.top = `${EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN}px`; + if (isCodeCellRenderTemplate(templateData)) { + templateData.focusIndicatorRight.style.top = `${EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN}px`; + } } else { templateData.container.classList.remove('cell-has-toolbar-actions'); templateData.focusIndicator.style.top = `${EDITOR_TOP_MARGIN}px`; + if (isCodeCellRenderTemplate(templateData)) { + templateData.focusIndicatorRight.style.top = `${EDITOR_TOP_MARGIN}px`; + } } } }; @@ -336,7 +338,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); const toolbar = disposables.add(this.createToolbar(container)); - const focusIndicator = DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); + const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side')); focusIndicator.setAttribute('draggable', 'true'); const codeInnerContent = DOM.append(container, $('.cell.code')); @@ -715,7 +717,8 @@ export class CellLanguageStatusBarItem extends Disposable { } private render(): void { - this.labelElement.textContent = this.modeService.getLanguageName(this.cell!.language!); + const modeId = this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; + this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); } } @@ -803,11 +806,6 @@ class CodeCellDragImageRenderer { return null; } - const focusIndicator = dragImageContainer.querySelector('.notebook-cell-focus-indicator') as HTMLElement; - if (focusIndicator) { - focusIndicator.style.height = '40px'; - } - const richEditorText = new EditorTextRenderer().getRichText(editor, new Range(1, 1, 1, 1000)); if (!richEditorText) { return null; @@ -865,8 +863,13 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende container.classList.add('code-cell-row'); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); + + const focusIndicatorTop = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); + DOM.append( + DOM.append(focusIndicatorTop, $('.cell-shadow-container.cell-shadow-container-top')), + $('.cell-shadow.cell-shadow-top')); const toolbar = disposables.add(this.createToolbar(container)); - const focusIndicator = DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); + const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side')); focusIndicator.setAttribute('draggable', 'true'); const cellContainer = DOM.append(container, $('.cell.code')); @@ -901,6 +904,10 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const timer = new TimerRenderer(statusBar.durationContainer); const outputContainer = DOM.append(container, $('.output')); + + const focusIndicatorRight = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right')); + focusIndicatorRight.setAttribute('draggable', 'true'); + const focusSinkElement = DOM.append(container, $('.cell-editor-focus-sink')); focusSinkElement.setAttribute('tabindex', '0'); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); @@ -908,6 +915,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService); DOM.append(bottomCellContainer, $('.separator')); + const focusIndicatorBottom = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom')); + DOM.append( + DOM.append(focusIndicatorBottom, $('.cell-shadow-container.cell-shadow-container-bottom')), + $('.cell-shadow.cell-shadow-bottom')); + const templateData: CodeCellRenderTemplate = { contextKeyService, container, @@ -918,6 +930,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende languageStatusBarItem: statusBar.languageStatusBarItem, progressBar, focusIndicator, + focusIndicatorRight, + focusIndicatorBottom, toolbar, betweenCellToolbar, focusSinkElement, @@ -956,13 +970,13 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.runToolbar.setActions([ this.instantiationService.createInstance(CancelCellAction) - ])(); + ]); } else { templateData.progressBar.hide(); templateData.runToolbar.setActions([ this.instantiationService.createInstance(ExecuteCellAction) - ])(); + ]); } } @@ -1024,6 +1038,13 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende DOM.toggleClass(templateData.container, 'cell-output-hover', element.outputIsHovered); } + private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { + templateData.focusIndicator.style.height = `${element.layoutInfo.indicatorHeight}px`; + templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; + templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - CELL_BOTTOM_MARGIN}px`; + templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; + } + renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { this.commonRenderElement(element, index, templateData); @@ -1042,14 +1063,13 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor.viewModel?.notebookDocument!, element)); - templateData.focusIndicator.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; + this.updateForLayout(element, templateData); elementDisposables.add(element.onDidChangeLayout(() => { - templateData.focusIndicator.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; + this.updateForLayout(element, templateData); })); this.updateForMetadata(element, templateData); + this.updateForHover(element, templateData); elementDisposables.add(element.onDidChangeState((e) => { if (e.metadataChanged) { this.updateForMetadata(element, templateData); @@ -1086,7 +1106,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende disposeElement(element: ICellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { templateData.elementDisposables.clear(); this.renderedEditors.delete(element); - templateData.focusIndicator.style.height = 'initial'; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 0bbb7773905..6dc8db80900 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -57,7 +57,7 @@ export class StatefullMarkdownCell extends Disposable { this._register(getResizesObserver(this.markdownContainer, undefined, () => { if (viewCell.editState === CellEditState.Preview) { - this.viewCell.totalHeight = templateData.container.clientHeight; + this.viewCell.renderedMarkdownHeight = templateData.container.clientHeight; } })).startObserving(); @@ -202,15 +202,13 @@ export class StatefullMarkdownCell extends Disposable { if (this.editor) { // switch from editing mode - const clientHeight = this.templateData.container.clientHeight; - this.viewCell.totalHeight = clientHeight; - this.notebookEditor.layoutNotebookCell(this.viewCell, clientHeight); + this.viewCell.renderedMarkdownHeight = this.templateData.container.clientHeight; + this.relayoutCell(); } else { // first time, readonly mode this.localDisposables.add(markdownRenderer.onDidUpdateRender(() => { - const clientHeight = this.templateData.container.clientHeight; - this.viewCell.totalHeight = clientHeight; - this.notebookEditor.layoutNotebookCell(this.viewCell, clientHeight); + this.viewCell.renderedMarkdownHeight = this.templateData.container.clientHeight; + this.relayoutCell(); })); this.localDisposables.add(this.viewCell.textBuffer.onDidChangeContent(() => { @@ -222,9 +220,8 @@ export class StatefullMarkdownCell extends Disposable { } })); - const clientHeight = this.templateData.container.clientHeight; - this.viewCell.totalHeight = clientHeight; - this.notebookEditor.layoutNotebookCell(this.viewCell, clientHeight); + this.viewCell.renderedMarkdownHeight = this.templateData.container.clientHeight; + this.relayoutCell(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 6f4df1bc656..5e344c75e85 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -262,6 +262,7 @@ function webviewPreloads() { const onWillDestroyOutput = createEmitter<[string | undefined /* namespace */, IDestroyCellInfo | undefined /* cell uri */]>(); const onDidCreateOutput = createEmitter<[string | undefined /* namespace */, ICreateCellInfo]>(); + const onDidReceiveMessage = createEmitter<[string, unknown]>(); const matchesNs = (namespace: string, query: string | undefined) => namespace === '*' || query === namespace || query === 'undefined'; @@ -271,7 +272,14 @@ function webviewPreloads() { } return { - postMessage: vscode.postMessage, + postMessage(message: unknown) { + vscode.postMessage({ + __vscode_notebook_message: true, + type: 'customRendererMessage', + rendererId: namespace, + message, + }); + }, setState(newState: T) { vscode.setState({ ...vscode.getState(), [namespace]: newState }); }, @@ -279,6 +287,7 @@ function webviewPreloads() { const state = vscode.getState(); return typeof state === 'object' && state ? state[namespace] as T : undefined; }, + onDidReceiveMessage: mapEmitter(onDidReceiveMessage, ([ns, data]) => ns === namespace ? data : dontEmit), onWillDestroyOutput: mapEmitter(onWillDestroyOutput, ([ns, data]) => matchesNs(namespace, ns) ? data : dontEmit), onDidCreateOutput: mapEmitter(onDidCreateOutput, ([ns, data]) => matchesNs(namespace, ns) ? data : dontEmit), }; @@ -406,6 +415,9 @@ function webviewPreloads() { focusFirstFocusableInCell(event.data.id); break; } + case 'customRendererMessage': + onDidReceiveMessage.fire([event.data.rendererId, event.data.message]); + break; } }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index d6089f55150..5b6804f4e89 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -9,7 +9,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -101,7 +101,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod // recompute this._ensureOutputsTop(); const outputTotalHeight = this._outputsTop!.getTotalValue(); - const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT; + const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT + CELL_BOTTOM_MARGIN; const indicatorHeight = this.editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.editorHeight + CELL_STATUSBAR_HEIGHT; const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 73f293399c3..70f4e000894 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -26,13 +26,18 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie return this._layoutInfo; } - set totalHeight(newHeight: number) { + set renderedMarkdownHeight(newHeight: number) { + const newTotalHeight = newHeight + BOTTOM_CELL_TOOLBAR_HEIGHT; + this.totalHeight = newTotalHeight; + } + + private set totalHeight(newHeight: number) { if (newHeight !== this.layoutInfo.totalHeight) { this.layoutChange({ totalHeight: newHeight }); } } - get totalHeight() { + private get totalHeight() { throw new Error('MarkdownCellViewModel.totalHeight is write only'); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 4c072434b1c..dc8b486b467 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -21,7 +21,7 @@ export interface IMainNotebookController { createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string, backupId?: string): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; executeNotebook(viewType: string, uri: URI, useAttachedKernel: boolean, token: CancellationToken): Promise; - onDidReceiveMessage(editorId: string, message: any): void; + onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: any): void; executeNotebookCell(uri: URI, handle: number, useAttachedKernel: boolean, token: CancellationToken): Promise; removeNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI, token: CancellationToken): Promise; @@ -65,13 +65,14 @@ export interface INotebookService { save(viewType: string, resource: URI, token: CancellationToken): Promise; saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise; backup(viewType: string, uri: URI, token: CancellationToken): Promise; - onDidReceiveMessage(viewType: string, editorId: string, message: any): void; + onDidReceiveMessage(viewType: string, editorId: string, rendererType: string | undefined, message: unknown): void; setToCopy(items: NotebookCellTextModel[]): void; getToCopy(): NotebookCellTextModel[] | undefined; // editor events addNotebookEditor(editor: IEditor): void; removeNotebookEditor(editor: IEditor): void; + getNotebookEditor(editorId: string): IEditor | undefined; listNotebookEditors(): readonly IEditor[]; listVisibleNotebookEditors(): readonly IEditor[]; listNotebookDocuments(): readonly NotebookTextModel[]; diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 475b54342dc..cf5753c9566 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -52,6 +52,9 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + uri?: URI | undefined; + textModel?: NotebookTextModel | undefined; + hasModel(): boolean { return true; } @@ -110,7 +113,19 @@ export class TestNotebookEditor implements INotebookEditor { isNotebookEditor = true; - postMessage(message: any): void { + postMessage(): void { + throw new Error('Method not implemented.'); + } + + toggleClassName(className: string): void { + throw new Error('Method not implemented.'); + } + + addClassName(className: string): void { + throw new Error('Method not implemented.'); + } + + removeClassName(className: string): void { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 28fe49d7593..5de7311b210 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -541,7 +541,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.toolbar.context = element; const actions = this.disposableActionFactory(element.setting); actions.forEach(a => template.elementDisposables?.add(a)); - template.toolbar.setActions([], [...this.settingActions, ...actions])(); + template.toolbar.setActions([], [...this.settingActions, ...actions]); this.fixToolbarIcon(template.toolbar); const setting = element.setting; diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 2da175275bd..d0fc0b65af4 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -74,19 +74,19 @@ const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint ({ - extensionDescription: info.extensionDescription, - url: info.getStarted!, - remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName - })), + getStarted.map((info: HelpInformation) => (new HelpItemValue(commandService, + info.extensionDescription, + (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName, + info.getStarted!) + )), quickInputService, environmentService, openerService, @@ -200,11 +200,11 @@ class HelpModel { helpItems.push(new HelpItem( documentationIcon, nls.localize('remote.help.documentation', "Read Documentation"), - documentation.map((info: HelpInformation) => ({ - extensionDescription: info.extensionDescription, - url: info.documentation!, - remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName - })), + documentation.map((info: HelpInformation) => (new HelpItemValue(commandService, + info.extensionDescription, + (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName, + info.documentation!) + )), quickInputService, environmentService, openerService, @@ -218,11 +218,11 @@ class HelpModel { helpItems.push(new HelpItem( feedbackIcon, nls.localize('remote.help.feedback', "Provide Feedback"), - feedback.map((info: HelpInformation) => ({ - extensionDescription: info.extensionDescription, - url: info.feedback!, - remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName - })), + feedback.map((info: HelpInformation) => (new HelpItemValue(commandService, + info.extensionDescription, + (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName, + info.feedback!) + )), quickInputService, environmentService, openerService, @@ -236,11 +236,11 @@ class HelpModel { helpItems.push(new HelpItem( reviewIssuesIcon, nls.localize('remote.help.issues', "Review Issues"), - issues.map((info: HelpInformation) => ({ - extensionDescription: info.extensionDescription, - url: info.issues!, - remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName - })), + issues.map((info: HelpInformation) => (new HelpItemValue(commandService, + info.extensionDescription, + (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName, + info.issues!) + )), quickInputService, environmentService, openerService, @@ -252,10 +252,10 @@ class HelpModel { helpItems.push(new IssueReporterItem( reportIssuesIcon, nls.localize('remote.help.report', "Report Issue"), - viewModel.helpInformation.map(info => ({ - extensionDescription: info.extensionDescription, - remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName - })), + viewModel.helpInformation.map(info => (new HelpItemValue(commandService, + info.extensionDescription, + (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName + ))), quickInputService, environmentService, commandService, @@ -269,12 +269,35 @@ class HelpModel { } } +class HelpItemValue { + private _url: string | undefined; + constructor(private commandService: ICommandService, public extensionDescription: IExtensionDescription, public remoteAuthority: string[] | undefined, private urlOrCommand?: string) { } + + get url(): Promise { + return new Promise(async (resolve) => { + if (this._url === undefined) { + if (this.urlOrCommand) { + let url = URI.parse(this.urlOrCommand); + if (url.authority) { + this._url = this.urlOrCommand; + } else { + this._url = await this.commandService.executeCommand(this.urlOrCommand); + } + } else { + this._url = ''; + } + } + resolve(this._url); + }); + } +} + abstract class HelpItemBase implements IHelpItem { public iconClasses: string[] = []; constructor( public icon: Codicon, public label: string, - public values: { extensionDescription: IExtensionDescription, url?: string, remoteAuthority: string[] | undefined }[], + public values: HelpItemValue[], private quickInputService: IQuickInputService, private environmentService: IWorkbenchEnvironmentService, private remoteExplorerService: IRemoteExplorerService @@ -285,17 +308,16 @@ abstract class HelpItemBase implements IHelpItem { async handleClick() { const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (!remoteAuthority) { - return; - } - for (let i = 0; i < this.remoteExplorerService.targetType.length; i++) { - if (startsWith(remoteAuthority, this.remoteExplorerService.targetType[i])) { - for (let value of this.values) { - if (value.remoteAuthority) { - for (let authority of value.remoteAuthority) { - if (startsWith(remoteAuthority, authority)) { - await this.takeAction(value.extensionDescription, value.url); - return; + if (remoteAuthority) { + for (let i = 0; i < this.remoteExplorerService.targetType.length; i++) { + if (startsWith(remoteAuthority, this.remoteExplorerService.targetType[i])) { + for (let value of this.values) { + if (value.remoteAuthority) { + for (let authority of value.remoteAuthority) { + if (startsWith(remoteAuthority, authority)) { + await this.takeAction(value.extensionDescription, await value.url); + return; + } } } } @@ -304,13 +326,13 @@ abstract class HelpItemBase implements IHelpItem { } if (this.values.length > 1) { - let actions = this.values.map(value => { + let actions = await Promise.all(this.values.map(async (value) => { return { label: value.extensionDescription.displayName || value.extensionDescription.identifier.value, - description: value.url, + description: await value.url, extensionDescription: value.extensionDescription }; - }); + })); const action = await this.quickInputService.pick(actions, { placeHolder: nls.localize('pickRemoteExtension', "Select url to open") }); @@ -318,7 +340,7 @@ abstract class HelpItemBase implements IHelpItem { await this.takeAction(action.extensionDescription, action.description); } } else { - await this.takeAction(this.values[0].extensionDescription, this.values[0].url); + await this.takeAction(this.values[0].extensionDescription, await this.values[0].url); } } @@ -329,7 +351,7 @@ class HelpItem extends HelpItemBase { constructor( icon: Codicon, label: string, - values: { extensionDescription: IExtensionDescription; url: string, remoteAuthority: string[] | undefined }[], + values: HelpItemValue[], quickInputService: IQuickInputService, environmentService: IWorkbenchEnvironmentService, private openerService: IOpenerService, @@ -347,7 +369,7 @@ class IssueReporterItem extends HelpItemBase { constructor( icon: Codicon, label: string, - values: { extensionDescription: IExtensionDescription; remoteAuthority: string[] | undefined }[], + values: HelpItemValue[], quickInputService: IQuickInputService, environmentService: IWorkbenchEnvironmentService, private commandService: ICommandService, diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index c386be60bba..a749425a97f 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -643,7 +643,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider('conflictsSources', ''); @@ -886,6 +886,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ type: 'separator' }); } items.push({ id: configureSyncCommand.id, label: configureSyncCommand.title }); + items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title }); + items.push({ id: SHOW_SYNCED_DATA_COMMAND_ID, label: localize('show synced data', "Preferences Sync: Show Synced Data") }); items.push({ type: 'separator' }); items.push({ id: syncNowCommand.id, label: syncNowCommand.title, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataAutoSyncService.canToggleEnablement()) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 8c8afdba348..d22800ce0ee 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -7,16 +7,15 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IViewsRegistry, Extensions, ITreeViewDescriptor, ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, IViewsService, TreeViewItemHandleArg, ViewContainer, IViewDescriptorService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; +import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, SyncStatus, IUserDataSyncResourceEnablementService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle as IResourceHandle, SyncStatus, IUserDataSyncResourceEnablementService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr, ContextKeyEqualsExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FolderThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; import { fromNow } from 'vs/base/common/date'; -import { pad } from 'vs/base/common/strings'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -35,12 +34,15 @@ import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CO import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { generateUuid } from 'vs/base/common/uuid'; +import { TreeView } from 'vs/workbench/contrib/views/browser/treeView'; +import { flatten } from 'vs/base/common/arrays'; export class UserDataSyncViewPaneContainer extends ViewPaneContainer { constructor( containerId: string, + @IDialogService private readonly dialogService: IDialogService, + @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, @ICommandService private readonly commandService: ICommandService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService, @@ -63,6 +65,24 @@ export class UserDataSyncViewPaneContainer extends ViewPaneContainer { ]; } + getSecondaryActions(): IAction[] { + return [ + new Action('workbench.actions.syncData.reset', localize('workbench.actions.syncData.reset', "Reset Synced Data"), undefined, true, () => this.reset()), + ]; + } + + private async reset(): Promise { + const result = await this.dialogService.confirm({ + message: localize('reset', "This will clear your synced data from the cloud and stop sync on all your devices."), + title: localize('reset title', "Reset Synced Data"), + type: 'info', + primaryButton: localize('reset button', "Reset"), + }); + if (result.confirmed) { + await this.userDataSyncWorkbenchService.turnoff(true); + } + } + } export class UserDataSyncDataViews extends Disposable { @@ -72,33 +92,28 @@ export class UserDataSyncDataViews extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); this.registerViews(container); } private registerViews(container: ViewContainer): void { - const remoteView = this.registerDataView(container, true, true); - this.registerRemoteViewActions(remoteView); - - this.registerDataView(container, false, false); + this.registerSyncedDataView(container); this.registerMachinesView(container); + + this.registerActivityView(container, true); + this.registerActivityView(container, false); } - private registerDataView(container: ViewContainer, remote: boolean, showByDefault: boolean): TreeView { - const id = `workbench.views.sync.${remote ? 'remote' : 'local'}DataView`; - const showByDefaultContext = new RawContextKey(id, showByDefault); - const viewEnablementContext = showByDefaultContext.bindTo(this.contextKeyService); - const name = remote ? localize('remote title', "Synced Data") : localize('local title', "Local Backup"); + private registerSyncedDataView(container: ViewContainer): void { + const id = `workbench.views.syncedDataView`; + const name = localize('remote title', "Synced Data"); const treeView = this.instantiationService.createInstance(TreeView, id, name); - treeView.showCollapseAllAction = true; treeView.showRefreshAction = true; const disposable = treeView.onDidChangeVisibility(visible => { if (visible && !treeView.dataProvider) { disposable.dispose(); - treeView.dataProvider = remote ? this.instantiationService.createInstance(RemoteUserDataSyncHistoryViewDataProvider) - : this.instantiationService.createInstance(LocalUserDataSyncHistoryViewDataProvider); + treeView.dataProvider = this.instantiationService.createInstance(SyncedDataViewDataProvider); } }); this._register(Event.any(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, this.userDataAutoSyncService.onDidChangeEnablement)(() => treeView.refresh())); @@ -107,9 +122,9 @@ export class UserDataSyncDataViews extends Disposable { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), - when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS, showByDefaultContext), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS), canToggleVisibility: true, - canMoveView: true, + canMoveView: false, treeView, collapsed: false, order: 100, @@ -118,10 +133,8 @@ export class UserDataSyncDataViews extends Disposable { registerAction2(class extends Action2 { constructor() { super({ - id: remote ? SHOW_SYNCED_DATA_COMMAND_ID : 'workbench.userDataSync.actions.showLocalBackupData', - title: remote ? - { value: localize('workbench.action.showSyncRemoteBackup', "Show Synced Data"), original: `Show Synced Data` } - : { value: localize('workbench.action.showSyncLocalBackup', "Show Local Backup"), original: `Show Local Backup` }, + id: SHOW_SYNCED_DATA_COMMAND_ID, + title: { value: localize('workbench.action.showSyncRemoteBackup', "Show Synced Data"), original: `Show Synced Data` }, category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` }, menu: { id: MenuId.CommandPalette, @@ -135,7 +148,6 @@ export class UserDataSyncDataViews extends Disposable { const commandService = accessor.get(ICommandService); await commandService.executeCommand(ENABLE_SYNC_VIEWS_COMMAND_ID); - viewEnablementContext.set(true); const viewContainer = viewDescriptorService.getViewContainerByViewId(id); if (viewContainer) { @@ -155,7 +167,6 @@ export class UserDataSyncDataViews extends Disposable { }); this.registerDataViewActions(id); - return treeView; } private registerMachinesView(container: ViewContainer): void { @@ -178,7 +189,7 @@ export class UserDataSyncDataViews extends Disposable { ctorDescriptor: new SyncDescriptor(TreeViewPane), when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS), canToggleVisibility: true, - canMoveView: true, + canMoveView: false, treeView, collapsed: false, order: 200, @@ -225,6 +236,37 @@ export class UserDataSyncDataViews extends Disposable { } + private registerActivityView(container: ViewContainer, remote: boolean): void { + const id = `workbench.views.sync.${remote ? 'remote' : 'local'}Activity`; + const name = remote ? localize('remote sync activity title', "Sync Activity (Remote)") : localize('local sync activity title', "Sync Activity (Local)"); + const treeView = this.instantiationService.createInstance(TreeView, id, name); + treeView.showCollapseAllAction = true; + treeView.showRefreshAction = true; + const disposable = treeView.onDidChangeVisibility(visible => { + if (visible && !treeView.dataProvider) { + disposable.dispose(); + treeView.dataProvider = remote ? this.instantiationService.createInstance(RemoteUserDataSyncActivityViewDataProvider) + : this.instantiationService.createInstance(LocalUserDataSyncActivityViewDataProvider); + } + }); + this._register(Event.any(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, this.userDataAutoSyncService.onDidChangeEnablement)(() => treeView.refresh())); + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + viewsRegistry.registerViews([{ + id, + name, + ctorDescriptor: new SyncDescriptor(TreeViewPane), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS), + canToggleVisibility: true, + canMoveView: false, + treeView, + collapsed: false, + order: 300, + hideByDefault: true, + }], container); + + this.registerDataViewActions(id); + } + private registerDataViewActions(viewId: string) { registerAction2(class extends Action2 { constructor() { @@ -249,7 +291,7 @@ export class UserDataSyncDataViews extends Disposable { super({ id: `workbench.actions.sync.replaceCurrent`, title: localize('workbench.actions.sync.replaceCurrent', "Restore"), - icon: { id: 'codicon/cloud-download' }, + icon: { id: 'codicon/discard' }, menu: { id: MenuId.ViewItemContext, when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)), @@ -298,51 +340,23 @@ export class UserDataSyncDataViews extends Disposable { }); } - private registerRemoteViewActions(view: TreeView) { - this.registerResetAction(view); - } - - private registerResetAction(view: TreeView) { - registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.syncData.reset`, - title: localize('workbench.actions.syncData.reset', "Reset Synced Data"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', view.id)), - }, - }); - } - async run(accessor: ServicesAccessor): Promise { - const dialogService = accessor.get(IDialogService); - const userDataSyncWorkbenchService = accessor.get(IUserDataSyncWorkbenchService); - const result = await dialogService.confirm({ - message: localize('reset', "This will clear your synced data from the cloud and stop sync on all your devices."), - title: localize('reset title', "Reset Synced Data"), - type: 'info', - primaryButton: localize('reset button', "Reset"), - }); - if (result.confirmed) { - await userDataSyncWorkbenchService.turnoff(true); - await view.refresh(); - } - } - }); - } } -interface SyncResourceTreeItem extends ITreeItem { - resource: SyncResource; - resourceHandle: ISyncResourceHandle; +interface ISyncResourceHandle extends IResourceHandle { + syncResource: SyncResource } -abstract class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { +interface SyncResourceHandleTreeItem extends ITreeItem { + syncResourceHandle: ISyncResourceHandle; +} + +abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProvider { + + private syncResourceHandlesPromise: Promise | undefined; constructor( @IUserDataSyncService protected readonly userDataSyncService: IUserDataSyncService, @IUserDataAutoSyncService protected readonly userDataAutoSyncService: IUserDataAutoSyncService, - @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @INotificationService private readonly notificationService: INotificationService, ) { } @@ -351,12 +365,8 @@ abstract class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvi if (!element) { return await this.getRoots(); } - const syncResource = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource; - if (syncResource) { - return await this.getChildrenForSyncResource(syncResource); - } - if ((element).resourceHandle) { - return await this.getChildrenForSyncResourceTreeItem(element); + if ((element).syncResourceHandle) { + return await this.getChildrenForSyncResourceTreeItem(element); } return []; } catch (error) { @@ -365,44 +375,27 @@ abstract class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvi } } - protected async getRoots(): Promise { - return ALL_SYNC_RESOURCES.map(resourceKey => ({ - handle: resourceKey, - collapsibleState: TreeItemCollapsibleState.Collapsed, - label: { label: getSyncAreaLabel(resourceKey) }, - description: !this.userDataAutoSyncService.isEnabled() || this.userDataSyncResourceEnablementService.isResourceEnabled(resourceKey) ? undefined : localize('not syncing', "Not syncing"), - themeIcon: FolderThemeIcon, - contextValue: resourceKey - })); + private async getRoots(): Promise { + this.syncResourceHandlesPromise = undefined; + + const syncResourceHandles = await this.getSyncResourceHandles(); + + return syncResourceHandles.map(syncResourceHandle => { + const handle = JSON.stringify({ resource: syncResourceHandle.uri.toString(), syncResource: syncResourceHandle.syncResource }); + return { + handle, + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: getSyncAreaLabel(syncResourceHandle.syncResource) }, + description: fromNow(syncResourceHandle.created, true), + themeIcon: FolderThemeIcon, + syncResourceHandle, + contextValue: `sync-resource-${syncResourceHandle.syncResource}` + }; + }); } - protected async getChildrenForSyncResource(syncResource: SyncResource): Promise { - const refHandles = await this.getSyncResourceHandles(syncResource); - if (refHandles.length) { - return refHandles.map(({ uri, created }) => { - const handle = JSON.stringify({ resource: uri.toString(), syncResource }); - return { - handle, - collapsibleState: TreeItemCollapsibleState.Collapsed, - label: { label: label(new Date(created)) }, - description: fromNow(created, true), - resourceUri: uri, - resource: syncResource, - resourceHandle: { uri, created }, - contextValue: `sync-resource-${syncResource}` - }; - }); - } else { - return [{ - handle: generateUuid(), - collapsibleState: TreeItemCollapsibleState.None, - label: { label: localize('no data', "No Data") }, - }]; - } - } - - protected async getChildrenForSyncResourceTreeItem(element: SyncResourceTreeItem): Promise { - const associatedResources = await this.userDataSyncService.getAssociatedResources((element).resource, (element).resourceHandle); + protected async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { + const associatedResources = await this.userDataSyncService.getAssociatedResources((element).syncResourceHandle.syncResource, (element).syncResourceHandle); return associatedResources.map(({ resource, comparableResource }) => { const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource?.toString() }); return { @@ -410,33 +403,42 @@ abstract class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvi collapsibleState: TreeItemCollapsibleState.None, resourceUri: resource, command: { id: `workbench.actions.sync.commpareWithLocal`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, - contextValue: `sync-associatedResource-${(element).resource}` + contextValue: `sync-associatedResource-${(element).syncResourceHandle.syncResource}` }; }); } - protected abstract getSyncResourceHandles(syncResource: SyncResource): Promise; + private getSyncResourceHandles(): Promise { + if (this.syncResourceHandlesPromise === undefined) { + this.syncResourceHandlesPromise = Promise.all(ALL_SYNC_RESOURCES.map(async syncResource => { + const resourceHandles = await this.getResourceHandles(syncResource); + return resourceHandles.map(resourceHandle => ({ ...resourceHandle, syncResource })); + })).then(result => flatten(result).sort((a, b) => b.created - a.created)); + } + return this.syncResourceHandlesPromise; + } + + protected abstract getResourceHandles(syncResource: SyncResource): Promise; } -class LocalUserDataSyncHistoryViewDataProvider extends UserDataSyncHistoryViewDataProvider { +class LocalUserDataSyncActivityViewDataProvider extends UserDataSyncActivityViewDataProvider { - protected getSyncResourceHandles(syncResource: SyncResource): Promise { + protected getResourceHandles(syncResource: SyncResource): Promise { return this.userDataSyncService.getLocalSyncResourceHandles(syncResource); } } -class RemoteUserDataSyncHistoryViewDataProvider extends UserDataSyncHistoryViewDataProvider { +class RemoteUserDataSyncActivityViewDataProvider extends UserDataSyncActivityViewDataProvider { private machinesPromise: Promise | undefined; constructor( @IUserDataSyncService userDataSyncService: IUserDataSyncService, @IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService, - @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, @INotificationService notificationService: INotificationService, ) { - super(userDataSyncService, userDataAutoSyncService, userDataSyncResourceEnablementService, notificationService); + super(userDataSyncService, userDataAutoSyncService, notificationService); } async getChildren(element?: ITreeItem): Promise { @@ -453,27 +455,30 @@ class RemoteUserDataSyncHistoryViewDataProvider extends UserDataSyncHistoryViewD return this.machinesPromise; } - protected getSyncResourceHandles(syncResource: SyncResource): Promise { + protected getResourceHandles(syncResource: SyncResource): Promise { return this.userDataSyncService.getRemoteSyncResourceHandles(syncResource); } - protected async getChildrenForSyncResourceTreeItem(element: SyncResourceTreeItem): Promise { + protected async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { const children = await super.getChildrenForSyncResourceTreeItem(element); - const machineId = await this.userDataSyncService.getMachineId(element.resource, element.resourceHandle); + const machineId = await this.userDataSyncService.getMachineId(element.syncResourceHandle.syncResource, element.syncResourceHandle); if (machineId) { const machines = await this.getMachines(); const machine = machines.find(({ id }) => id === machineId); - children.push({ - handle: machineId, - label: { label: machine?.name || machineId }, - collapsibleState: TreeItemCollapsibleState.None, - themeIcon: Codicon.vm, - }); + children[0].description = machine?.isCurrent ? localize('current', "Current") : machine?.name; } return children; } } +class SyncedDataViewDataProvider extends RemoteUserDataSyncActivityViewDataProvider { + + protected async getResourceHandles(syncResource: SyncResource): Promise { + const resourceHandles = await this.userDataSyncService.getRemoteSyncResourceHandles(syncResource); + return resourceHandles.length ? [resourceHandles[0]] : []; + } +} + class UserDataSyncMachinesViewDataProvider implements ITreeViewDataProvider { private machinesPromise: Promise | undefined; @@ -582,10 +587,3 @@ class UserDataSyncMachinesViewDataProvider implements ITreeViewDataProvider { }); } } - -function label(date: Date): string { - return date.toLocaleDateString() + - ' ' + pad(date.getHours(), 2) + - ':' + pad(date.getMinutes(), 2) + - ':' + pad(date.getSeconds(), 2); -} diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/contrib/views/browser/media/views.css similarity index 100% rename from src/vs/workbench/browser/parts/views/media/views.css rename to src/vs/workbench/contrib/views/browser/media/views.css diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts new file mode 100644 index 00000000000..12091f7c71e --- /dev/null +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -0,0 +1,1010 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/views'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import * as DOM from 'vs/base/browser/dom'; +import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; +import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { URI } from 'vs/base/common/uri'; +import { dirname, basename } from 'vs/base/common/resources'; +import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; +import { FileKind } from 'vs/platform/files/common/files'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { localize } from 'vs/nls'; +import { timeout } from 'vs/base/common/async'; +import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; +import { isString } from 'vs/base/common/types'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; + +class Root implements ITreeItem { + label = { label: 'root' }; + handle = '0'; + parentHandle: string | undefined = undefined; + collapsibleState = TreeItemCollapsibleState.Expanded; + children: ITreeItem[] | undefined = undefined; +} + +const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); + +class Tree extends WorkbenchAsyncDataTree { } + +export class TreeView extends Disposable implements ITreeView { + + private isVisible: boolean = false; + private _hasIconForParentNode = false; + private _hasIconForLeafNode = false; + + private readonly collapseAllContextKey: RawContextKey; + private readonly collapseAllContext: IContextKey; + private readonly refreshContextKey: RawContextKey; + private readonly refreshContext: IContextKey; + + private focused: boolean = false; + private domNode!: HTMLElement; + private treeContainer!: HTMLElement; + private _messageValue: string | undefined; + private _canSelectMany: boolean = false; + private messageElement!: HTMLDivElement; + private tree: Tree | undefined; + private treeLabels: ResourceLabels | undefined; + + private root: ITreeItem; + private elementsToRefresh: ITreeItem[] = []; + + private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); + readonly onDidExpandItem: Event = this._onDidExpandItem.event; + + private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); + readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; + + private _onDidChangeSelection: Emitter = this._register(new Emitter()); + readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + + private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + + private readonly _onDidChangeActions: Emitter = this._register(new Emitter()); + readonly onDidChangeActions: Event = this._onDidChangeActions.event; + + private readonly _onDidChangeWelcomeState: Emitter = this._register(new Emitter()); + readonly onDidChangeWelcomeState: Event = this._onDidChangeWelcomeState.event; + + private readonly _onDidChangeTitle: Emitter = this._register(new Emitter()); + readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; + + private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); + + constructor( + readonly id: string, + private _title: string, + @IThemeService private readonly themeService: IThemeService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProgressService protected readonly progressService: IProgressService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @INotificationService private readonly notificationService: INotificationService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(); + this.root = new Root(); + this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, false); + this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); + this.refreshContextKey = new RawContextKey(`treeView.${this.id}.enableRefresh`, false); + this.refreshContext = this.refreshContextKey.bindTo(contextKeyService); + + this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); + this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('explorer.decorations')) { + this.doRefresh([this.root]); /** soft refresh **/ + } + })); + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); + } + })); + this.registerActions(); + + this.create(); + } + + get viewContainer(): ViewContainer { + return this.viewDescriptorService.getViewContainerByViewId(this.id)!; + } + + get viewLocation(): ViewContainerLocation { + return this.viewDescriptorService.getViewLocationById(this.id)!; + } + + private _dataProvider: ITreeViewDataProvider | undefined; + get dataProvider(): ITreeViewDataProvider | undefined { + return this._dataProvider; + } + + set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { + if (this.tree === undefined) { + this.createTree(); + } + + if (dataProvider) { + this._dataProvider = new class implements ITreeViewDataProvider { + private _isEmpty: boolean = true; + private _onDidChangeEmpty: Emitter = new Emitter(); + public onDidChangeEmpty: Event = this._onDidChangeEmpty.event; + + get isTreeEmpty(): boolean { + return this._isEmpty; + } + + async getChildren(node: ITreeItem): Promise { + let children: ITreeItem[]; + if (node && node.children) { + children = node.children; + } else { + children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); + node.children = children; + } + if (node instanceof Root) { + const oldEmpty = this._isEmpty; + this._isEmpty = children.length === 0; + if (oldEmpty !== this._isEmpty) { + this._onDidChangeEmpty.fire(); + } + } + return children; + } + }; + if (this._dataProvider.onDidChangeEmpty) { + this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); + } + this.updateMessage(); + this.refresh(); + } else { + this._dataProvider = undefined; + this.updateMessage(); + } + + this._onDidChangeWelcomeState.fire(); + } + + private _message: string | undefined; + get message(): string | undefined { + return this._message; + } + + set message(message: string | undefined) { + this._message = message; + this.updateMessage(); + this._onDidChangeWelcomeState.fire(); + } + + get title(): string { + return this._title; + } + + set title(name: string) { + this._title = name; + this._onDidChangeTitle.fire(this._title); + } + + get canSelectMany(): boolean { + return this._canSelectMany; + } + + set canSelectMany(canSelectMany: boolean) { + this._canSelectMany = canSelectMany; + } + + get hasIconForParentNode(): boolean { + return this._hasIconForParentNode; + } + + get hasIconForLeafNode(): boolean { + return this._hasIconForLeafNode; + } + + get visible(): boolean { + return this.isVisible; + } + + get showCollapseAllAction(): boolean { + return !!this.collapseAllContext.get(); + } + + set showCollapseAllAction(showCollapseAllAction: boolean) { + this.collapseAllContext.set(showCollapseAllAction); + } + + get showRefreshAction(): boolean { + return !!this.refreshContext.get(); + } + + set showRefreshAction(showRefreshAction: boolean) { + this.refreshContext.set(showRefreshAction); + } + + private registerActions() { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.refresh`, + title: localize('refresh', "Refresh"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER - 1, + }, + icon: { id: 'codicon/refresh' } + }); + } + async run(): Promise { + return that.refresh(); + } + })); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER, + }, + icon: { id: 'codicon/collapse-all' } + }); + } + async run(): Promise { + if (that.tree) { + return new CollapseAllAction(that.tree, true).run(); + } + } + })); + } + + setVisibility(isVisible: boolean): void { + isVisible = !!isVisible; + if (this.isVisible === isVisible) { + return; + } + + this.isVisible = isVisible; + + if (this.tree) { + if (this.isVisible) { + DOM.show(this.tree.getHTMLElement()); + } else { + DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it + } + + if (this.isVisible && this.elementsToRefresh.length) { + this.doRefresh(this.elementsToRefresh); + this.elementsToRefresh = []; + } + } + + this._onDidChangeVisibility.fire(this.isVisible); + } + + focus(reveal: boolean = true): void { + if (this.tree && this.root.children && this.root.children.length > 0) { + // Make sure the current selected element is revealed + const selectedElement = this.tree.getSelection()[0]; + if (selectedElement && reveal) { + this.tree.reveal(selectedElement, 0.5); + } + + // Pass Focus to Viewer + this.tree.domFocus(); + } else if (this.tree) { + this.tree.domFocus(); + } else { + this.domNode.focus(); + } + } + + show(container: HTMLElement): void { + DOM.append(container, this.domNode); + } + + private create() { + this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); + this.messageElement = DOM.append(this.domNode, DOM.$('.message')); + this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); + DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); + DOM.addClass(this.treeContainer, 'show-file-icons'); + const focusTracker = this._register(DOM.trackFocus(this.domNode)); + this._register(focusTracker.onDidFocus(() => this.focused = true)); + this._register(focusTracker.onDidBlur(() => this.focused = false)); + } + + private createTree() { + const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; + const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); + this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); + const aligner = new Aligner(this.themeService); + const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); + const widgetAriaLabel = this._title; + + this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer], + dataSource, { + identityProvider: new TreeViewIdentityProvider(), + accessibilityProvider: { + getAriaLabel(element: ITreeItem): string { + if (element.accessibilityInformation) { + return element.accessibilityInformation.label; + } + + return isString(element.tooltip) ? element.tooltip : element.label ? element.label.label : ''; + }, + getRole(element: ITreeItem): string | undefined { + return element.accessibilityInformation?.role ?? 'treeitem'; + }, + getWidgetAriaLabel(): string { + return widgetAriaLabel; + } + }, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (item: ITreeItem) => { + return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); + } + }, + expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command, + collapseByDefault: (e: ITreeItem): boolean => { + return e.collapsibleState !== TreeItemCollapsibleState.Expanded; + }, + multipleSelectionSupport: this.canSelectMany, + overrideStyles: { + listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND + } + }) as WorkbenchAsyncDataTree); + aligner.tree = this.tree; + const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()); + renderer.actionRunner = actionRunner; + + this.tree.contextKeyService.createKey(this.id, true); + this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); + this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree.onDidChangeCollapseState(e => { + if (!e.node.element) { + return; + } + + const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; + if (e.node.collapsed) { + this._onDidCollapseItem.fire(element); + } else { + this._onDidExpandItem.fire(element); + } + })); + this.tree.setInput(this.root).then(() => this.updateContentAreas()); + + this._register(this.tree.onDidOpen(e => { + if (!e.browserEvent) { + return; + } + const selection = this.tree!.getSelection(); + if ((selection.length === 1) && selection[0].command) { + this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || [])); + } + })); + + } + + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { + const node: ITreeItem | null = treeEvent.element; + if (node === null) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.tree!.setFocus([node]); + const actions = treeMenus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + + getActions: () => actions, + + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree!.domFocus(); + } + }, + + getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), + + actionRunner + }); + } + + protected updateMessage(): void { + if (this._message) { + this.showMessage(this._message); + } else if (!this.dataProvider) { + this.showMessage(noDataProviderMessage); + } else { + this.hideMessage(); + } + this.updateContentAreas(); + } + + private showMessage(message: string): void { + DOM.removeClass(this.messageElement, 'hide'); + this.resetMessageElement(); + this._messageValue = message; + if (!isFalsyOrWhitespace(this._message)) { + this.messageElement.textContent = this._messageValue; + } + this.layout(this._height, this._width); + } + + private hideMessage(): void { + this.resetMessageElement(); + DOM.addClass(this.messageElement, 'hide'); + this.layout(this._height, this._width); + } + + private resetMessageElement(): void { + DOM.clearNode(this.messageElement); + } + + private _height: number = 0; + private _width: number = 0; + layout(height: number, width: number) { + if (height && width) { + this._height = height; + this._width = width; + const treeHeight = height - DOM.getTotalHeight(this.messageElement); + this.treeContainer.style.height = treeHeight + 'px'; + if (this.tree) { + this.tree.layout(treeHeight, width); + } + } + } + + getOptimalWidth(): number { + if (this.tree) { + const parentNode = this.tree.getHTMLElement(); + const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); + return DOM.getLargestChildWidth(parentNode, childNodes); + } + return 0; + } + + async refresh(elements?: ITreeItem[]): Promise { + if (this.dataProvider && this.tree) { + if (this.refreshing) { + await Event.toPromise(this._onDidCompleteRefresh.event); + } + if (!elements) { + elements = [this.root]; + // remove all waiting elements to refresh if root is asked to refresh + this.elementsToRefresh = []; + } + for (const element of elements) { + element.children = undefined; // reset children + } + if (this.isVisible) { + return this.doRefresh(elements); + } else { + if (this.elementsToRefresh.length) { + const seen: Set = new Set(); + this.elementsToRefresh.forEach(element => seen.add(element.handle)); + for (const element of elements) { + if (!seen.has(element.handle)) { + this.elementsToRefresh.push(element); + } + } + } else { + this.elementsToRefresh.push(...elements); + } + } + } + return undefined; + } + + async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { + const tree = this.tree; + if (tree) { + itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; + await Promise.all(itemOrItems.map(element => { + return tree.expand(element, false); + })); + } + } + + setSelection(items: ITreeItem[]): void { + if (this.tree) { + this.tree.setSelection(items); + } + } + + setFocus(item: ITreeItem): void { + if (this.tree) { + this.focus(); + this.tree.setFocus([item]); + } + } + + async reveal(item: ITreeItem): Promise { + if (this.tree) { + return this.tree.reveal(item); + } + } + + private refreshing: boolean = false; + private async doRefresh(elements: ITreeItem[]): Promise { + const tree = this.tree; + if (tree && this.visible) { + this.refreshing = true; + await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); + this.refreshing = false; + this._onDidCompleteRefresh.fire(); + this.updateContentAreas(); + if (this.focused) { + this.focus(false); + } + } + } + + private updateContentAreas(): void { + const isTreeEmpty = !this.root.children || this.root.children.length === 0; + // Hide tree container only when there is a message and tree is empty and not refreshing + if (this._messageValue && isTreeEmpty && !this.refreshing) { + DOM.addClass(this.treeContainer, 'hide'); + this.domNode.setAttribute('tabindex', '0'); + } else { + DOM.removeClass(this.treeContainer, 'hide'); + this.domNode.removeAttribute('tabindex'); + } + } +} + +class TreeViewIdentityProvider implements IIdentityProvider { + getId(element: ITreeItem): { toString(): string; } { + return element.handle; + } +} + +class TreeViewDelegate implements IListVirtualDelegate { + + getHeight(element: ITreeItem): number { + return TreeRenderer.ITEM_HEIGHT; + } + + getTemplateId(element: ITreeItem): string { + return TreeRenderer.TREE_TEMPLATE_ID; + } +} + +class TreeDataSource implements IAsyncDataSource { + + constructor( + private treeView: ITreeView, + private withProgress: (task: Promise) => Promise + ) { + } + + hasChildren(element: ITreeItem): boolean { + return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None); + } + + async getChildren(element: ITreeItem): Promise { + if (this.treeView.dataProvider) { + return this.withProgress(this.treeView.dataProvider.getChildren(element)); + } + return []; + } +} + +// todo@joh,sandy make this proper and contributable from extensions +registerThemingParticipant((theme, collector) => { + + const matchBackgroundColor = theme.getColor(listFilterMatchHighlight); + if (matchBackgroundColor) { + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); + collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); + } + const matchBorderColor = theme.getColor(listFilterMatchHighlightBorder); + if (matchBorderColor) { + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); + collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); + } + const focusBorderColor = theme.getColor(focusBorder); + if (focusBorderColor) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`); + } + const codeBackground = theme.getColor(textCodeBlockBackground); + if (codeBackground) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`); + } +}); + +interface ITreeExplorerTemplateData { + elementDisposable: IDisposable; + container: HTMLElement; + resourceLabel: IResourceLabel; + icon: HTMLElement; + actionBar: ActionBar; +} + +class TreeRenderer extends Disposable implements ITreeRenderer { + static readonly ITEM_HEIGHT = 22; + static readonly TREE_TEMPLATE_ID = 'treeExplorer'; + + private _actionRunner: MultipleSelectionActionRunner | undefined; + private readonly hoverDelay: number; + + constructor( + private treeViewId: string, + private menus: TreeMenus, + private labels: ResourceLabels, + private actionViewItemProvider: IActionViewItemProvider, + private aligner: Aligner, + @IThemeService private readonly themeService: IThemeService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILabelService private readonly labelService: ILabelService, + @IHoverService private readonly hoverService: IHoverService + ) { + super(); + this.hoverDelay = this.configurationService.getValue('editor.hover.delay'); + } + + get templateId(): string { + return TreeRenderer.TREE_TEMPLATE_ID; + } + + set actionRunner(actionRunner: MultipleSelectionActionRunner) { + this._actionRunner = actionRunner; + } + + renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { + DOM.addClass(container, 'custom-view-tree-node-item'); + + const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); + + const resourceLabel = this.labels.create(container, { supportHighlights: true }); + const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: this.actionViewItemProvider + }); + + return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None }; + } + + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + const node = element.element; + const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; + const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : resource ? { label: basename(resource) } : undefined; + const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined; + const label = treeItemLabel ? treeItemLabel.label : undefined; + const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => { + if (start < 0) { + start = label.length + start; + } + if (end < 0) { + end = label.length + end; + } + if ((start >= label.length) || (end > label.length)) { + return ({ start: 0, end: 0 }); + } + if (start > end) { + const swap = start; + start = end; + end = swap; + } + return ({ start, end }); + }) : undefined; + const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark; + const iconUrl = icon ? URI.revive(icon) : null; + const title = node.tooltip ? isString(node.tooltip) ? node.tooltip : undefined : resource ? undefined : label; + + // reset + templateData.actionBar.clear(); + + if (resource || this.isFileKindThemeIcon(node.themeIcon)) { + const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); + templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) }); + } else { + templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) }); + } + + templateData.icon.title = title ? title : ''; + + if (iconUrl) { + templateData.icon.className = 'custom-view-tree-node-item-icon'; + templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); + + } else { + let iconClass: string | undefined; + if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) { + iconClass = ThemeIcon.asClassName(node.themeIcon); + } + templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : ''; + templateData.icon.style.backgroundImage = ''; + } + + templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; + templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + if (this._actionRunner) { + templateData.actionBar.actionRunner = this._actionRunner; + } + this.setAlignment(templateData.container, node); + const disposableStore = new DisposableStore(); + templateData.elementDisposable = disposableStore; + disposableStore.add(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); + this.setupHovers(node.tooltip, templateData.container, disposableStore); + } + + private setupHovers(tooltip: string | IMarkdownString | undefined, htmlElement: HTMLElement, disposableStore: DisposableStore): void { + if (!tooltip || isString(tooltip)) { + return; + } + const text: IMarkdownString = tooltip; + const hoverService = this.hoverService; + const hoverDelay = this.hoverDelay; + function mouseOver(this: HTMLElement, e: MouseEvent): any { + let isHovering = true; + function mouseLeave(this: HTMLElement, e: MouseEvent): any { + isHovering = false; + } + this.addEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave, { passive: true }); + setTimeout(() => { + if (isHovering) { + hoverService.showHover({ text, target: this }); + } + this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave); + }, hoverDelay); + } + htmlElement.addEventListener(DOM.EventType.MOUSE_OVER, mouseOver, { passive: true }); + disposableStore.add({ + dispose: () => { + htmlElement.removeEventListener(DOM.EventType.MOUSE_OVER, mouseOver); + } + }); + } + + private setAlignment(container: HTMLElement, treeItem: ITreeItem) { + DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); + } + + private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { + if (icon) { + return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id; + } else { + return false; + } + } + + private getFileKind(node: ITreeItem): FileKind { + if (node.themeIcon) { + switch (node.themeIcon.id) { + case FileThemeIcon.id: + return FileKind.FILE; + case FolderThemeIcon.id: + return FileKind.FOLDER; + } + } + return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; + } + + disposeElement(resource: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + } + + disposeTemplate(templateData: ITreeExplorerTemplateData): void { + templateData.resourceLabel.dispose(); + templateData.actionBar.dispose(); + templateData.elementDisposable.dispose(); + } +} + +class Aligner extends Disposable { + private _tree: WorkbenchAsyncDataTree | undefined; + + constructor(private themeService: IThemeService) { + super(); + } + + set tree(tree: WorkbenchAsyncDataTree) { + this._tree = tree; + } + + public alignIconWithTwisty(treeItem: ITreeItem): boolean { + if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) { + return false; + } + if (!this.hasIcon(treeItem)) { + return false; + } + + if (this._tree) { + const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); + if (this.hasIcon(parent)) { + return false; + } + return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); + } else { + return false; + } + } + + private hasIcon(node: ITreeItem): boolean { + const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark; + if (icon) { + return true; + } + if (node.resourceUri || node.themeIcon) { + const fileIconTheme = this.themeService.getFileIconTheme(); + const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None; + if (isFolder) { + return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons; + } + return fileIconTheme.hasFileIcons; + } + return false; + } +} + +class MultipleSelectionActionRunner extends ActionRunner { + + constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) { + super(); + this._register(this.onDidRun(e => { + if (e.error) { + notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id)); + } + })); + } + + runAction(action: IAction, context: TreeViewItemHandleArg): Promise { + const selection = this.getSelectedResources(); + let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; + let actionInSelected: boolean = false; + if (selection.length > 1) { + selectionHandleArgs = selection.map(selected => { + if (selected.handle === context.$treeItemHandle) { + actionInSelected = true; + } + return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle }; + }); + } + + if (!actionInSelected) { + selectionHandleArgs = undefined; + } + + return action.run(...[context, selectionHandleArgs]); + } +} + +class TreeMenus extends Disposable implements IDisposable { + + constructor( + private id: string, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { + super(); + } + + getResourceActions(element: ITreeItem): IAction[] { + return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary; + } + + getResourceContextActions(element: ITreeItem): IAction[] { + return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary; + } + + private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } { + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('view', this.id); + contextKeyService.createKey(context.key, context.value); + + const menu = this.menuService.createMenu(menuId, contextKeyService); + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + + menu.dispose(); + contextKeyService.dispose(); + + return result; + } +} + +export class CustomTreeView extends TreeView { + + private activated: boolean = false; + + constructor( + id: string, + title: string, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService, + @IConfigurationService configurationService: IConfigurationService, + @IProgressService progressService: IProgressService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService); + } + + setVisibility(isVisible: boolean): void { + super.setVisibility(isVisible); + if (this.visible) { + this.activate(); + } + } + + private activate() { + if (!this.activated) { + this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + .then(() => timeout(2000)) + .then(() => { + this.updateMessage(); + }); + this.activated = true; + } + } +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts index 3ed7a1964cb..6d0ca158442 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -144,6 +144,6 @@ export class ReloadWebviewAction extends Action { export function getFocusedWebviewEditor(accessor: ServicesAccessor): Webview | undefined { const editorService = accessor.get(IEditorService); - const activeEditor = editorService.activeEditorPane; + const activeEditor = editorService.activeEditor; return activeEditor instanceof WebviewInput ? activeEditor.webview : undefined; } diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/index.html b/src/vs/workbench/contrib/webview/electron-browser/pre/index.html new file mode 100644 index 00000000000..80ba98f6bd0 --- /dev/null +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/index.html @@ -0,0 +1,11 @@ + + + + + Virtual Document + + + + + + diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts index 56d95808d70..7c27fad1810 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isMacintosh } from 'vs/base/common/platform'; +import { MultiCommand, RedoCommand, SelectAllCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -15,7 +15,6 @@ import { getFocusedWebviewEditor } from 'vs/workbench/contrib/webview/browser/we import * as webviewCommands from 'vs/workbench/contrib/webview/electron-browser/webviewCommands'; import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; import { ElectronWebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService'; -import { UndoCommand, RedoCommand } from 'vs/editor/browser/editorExtensions'; registerSingleton(IWebviewService, ElectronWebviewService, true); @@ -26,53 +25,40 @@ actionRegistry.registerWorkbenchAction( webviewCommands.OpenWebviewDeveloperToolsAction.ALIAS, webviewDeveloperCategory); -if (isMacintosh) { - function getActiveElectronBasedWebview(accessor: ServicesAccessor): ElectronWebviewBasedWebview | undefined { - const webview = getFocusedWebviewEditor(accessor); - if (!webview) { - return undefined; - } - - if (webview instanceof ElectronWebviewBasedWebview) { - return webview; - } else if ('getInnerWebview' in (webview as WebviewOverlay)) { - const innerWebview = (webview as WebviewOverlay).getInnerWebview(); - if (innerWebview instanceof ElectronWebviewBasedWebview) { - return innerWebview; - } - } - +function getActiveElectronBasedWebview(accessor: ServicesAccessor): ElectronWebviewBasedWebview | undefined { + const webview = getFocusedWebviewEditor(accessor); + if (!webview) { return undefined; } - function withWebview(accessor: ServicesAccessor, f: (webviewe: ElectronWebviewBasedWebview) => void) { + if (webview instanceof ElectronWebviewBasedWebview) { + return webview; + } else if ('getInnerWebview' in (webview as WebviewOverlay)) { + const innerWebview = (webview as WebviewOverlay).getInnerWebview(); + if (innerWebview instanceof ElectronWebviewBasedWebview) { + return innerWebview; + } + } + + return undefined; +} + +const PRIORITY = 100; + +function overrideCommandForWebview(command: MultiCommand | undefined, f: (webview: ElectronWebviewBasedWebview) => void) { + command?.addImplementation(PRIORITY, accessor => { const webview = getActiveElectronBasedWebview(accessor); if (webview) { f(webview); return true; } return false; - } - - const PRIORITY = 100; - - UndoCommand.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.undo()); - }); - - RedoCommand.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.redo()); - }); - - CopyAction?.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.copy()); - }); - - PasteAction?.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.paste()); - }); - - CutAction?.addImplementation(PRIORITY, accessor => { - return withWebview(accessor, webview => webview.cut()); }); } + +overrideCommandForWebview(UndoCommand, webview => webview.undo()); +overrideCommandForWebview(RedoCommand, webview => webview.redo()); +overrideCommandForWebview(SelectAllCommand, webview => webview.selectAll()); +overrideCommandForWebview(CopyAction, webview => webview.copy()); +overrideCommandForWebview(PasteAction, webview => webview.paste()); +overrideCommandForWebview(CutAction, webview => webview.cut()); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 4f3ce42ca14..bc31d6d54e9 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -289,7 +289,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme } this.element!.preload = require.toUrl('./pre/electron-index.js'); - this.element!.src = 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%20role%3D%22document%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E'; + this.element!.src = `${Schemas.vscodeWebview}://${id}/index.html`; } protected createElement(options: WebviewOptions) { diff --git a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts index f50e9d2c8bc..7b6e2e716b8 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts @@ -40,7 +40,7 @@ export default () => `
  • ${escape(localize('welcomePage.tipsAndTricks', "Tips and Tricks"))}
  • ${escape(localize('welcomePage.productDocumentation', "Product documentation"))}
  • ${escape(localize('welcomePage.gitHubRepository', "GitHub repository"))}
  • -
  • ${escape(localize('welcomePage.stackOverflow', "Stack Overflow"))}
  • +
  • ${escape(localize('welcomePage.stackOverflow', "Stack Overflow"))}
  • ${escape(localize('welcomePage.newsletterSignup', "Join our Newsletter"))}
  • diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 4ddd27af47f..ca37d322d6c 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -3,243 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mergeSort } from 'vs/base/common/arrays'; -import { dispose, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; -import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { localize } from 'vs/nls'; -import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; - -type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; - -class ModelEditTask implements IDisposable { - - public readonly model: ITextModel; - - protected _edits: IIdentifiedSingleEditOperation[]; - private _expectedModelVersionId: number | undefined; - protected _newEol: EndOfLineSequence | undefined; - - constructor(private readonly _modelReference: IReference) { - this.model = this._modelReference.object.textEditorModel; - this._edits = []; - } - - dispose() { - this._modelReference.dispose(); - } - - addEdit(resourceEdit: WorkspaceTextEdit): void { - this._expectedModelVersionId = resourceEdit.modelVersionId; - const { edit } = resourceEdit; - - if (typeof edit.eol === 'number') { - // honor eol-change - this._newEol = edit.eol; - } - if (!edit.range && !edit.text) { - // lacks both a range and the text - return; - } - if (Range.isEmpty(edit.range) && !edit.text) { - // no-op edit (replace empty range with empty text) - return; - } - - // create edit operation - let range: Range; - if (!edit.range) { - range = this.model.getFullModelRange(); - } else { - range = Range.lift(edit.range); - } - this._edits.push(EditOperation.replaceMove(range, edit.text)); - } - - validate(): ValidationResult { - if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) { - return { canApply: true }; - } - return { canApply: false, reason: this.model.uri }; - } - - getBeforeCursorState(): Selection[] | null { - return null; - } - - apply(): void { - if (this._edits.length > 0) { - this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - this.model.pushEditOperations(null, this._edits, () => null); - } - if (this._newEol !== undefined) { - this.model.pushEOL(this._newEol); - } - } -} - -class EditorEditTask extends ModelEditTask { - - private _editor: ICodeEditor; - - constructor(modelReference: IReference, editor: ICodeEditor) { - super(modelReference); - this._editor = editor; - } - - getBeforeCursorState(): Selection[] | null { - return this._editor.getSelections(); - } - - apply(): void { - if (this._edits.length > 0) { - this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - this._editor.executeEdits('', this._edits); - } - if (this._newEol !== undefined) { - if (this._editor.hasModel()) { - this._editor.getModel().pushEOL(this._newEol); - } - } - } -} - -class BulkEditModel implements IDisposable { - - private _edits = new Map(); - private _tasks: ModelEditTask[] | undefined; - - constructor( - private readonly _label: string | undefined, - private readonly _editor: ICodeEditor | undefined, - private readonly _progress: IProgress, - edits: WorkspaceTextEdit[], - @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, - @IUndoRedoService private readonly _undoRedoService: IUndoRedoService - ) { - edits.forEach(this._addEdit, this); - } - - dispose(): void { - if (this._tasks) { - dispose(this._tasks); - } - } - - private _addEdit(edit: WorkspaceTextEdit): void { - let array = this._edits.get(edit.resource.toString()); - if (!array) { - array = []; - this._edits.set(edit.resource.toString(), array); - } - array.push(edit); - } - - async prepare(): Promise { - - if (this._tasks) { - throw new Error('illegal state - already prepared'); - } - - this._tasks = []; - const promises: Promise[] = []; - - for (let [key, value] of this._edits) { - const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(async ref => { - let task: ModelEditTask; - let makeMinimal = false; - if (this._editor && this._editor.hasModel() && this._editor.getModel().uri.toString() === ref.object.textEditorModel.uri.toString()) { - task = new EditorEditTask(ref, this._editor); - makeMinimal = true; - } else { - task = new ModelEditTask(ref); - } - - for (const edit of value) { - if (makeMinimal) { - const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]); - if (!newEdits) { - task.addEdit(edit); - } else { - for (let moreMinialEdit of newEdits) { - task.addEdit({ ...edit, edit: moreMinialEdit }); - } - } - } else { - task.addEdit(edit); - } - } - - this._tasks!.push(task); - this._progress.report(undefined); - }); - promises.push(promise); - } - - await Promise.all(promises); - - return this; - } - - validate(): ValidationResult { - for (const task of this._tasks!) { - const result = task.validate(); - if (!result.canApply) { - return result; - } - } - return { canApply: true }; - } - - apply(): void { - const tasks = this._tasks!; - - if (tasks.length === 1) { - // This edit touches a single model => keep things simple - for (const task of tasks) { - task.model.pushStackElement(); - task.apply(); - task.model.pushStackElement(); - this._progress.report(undefined); - } - return; - } - - const multiModelEditStackElement = new MultiModelEditStackElement( - this._label || localize('workspaceEdit', "Workspace Edit"), - tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState())) - ); - this._undoRedoService.pushElement(multiModelEditStackElement); - - for (const task of tasks) { - task.apply(); - this._progress.report(undefined); - } - - multiModelEditStackElement.close(); - } -} +import { BulkTextEdits } from 'vs/workbench/services/bulkEdit/browser/bulkTextEdits'; +import { BulkFileEdits } from 'vs/workbench/services/bulkEdit/browser/bulkFileEdits'; +import { ResourceMap } from 'vs/base/common/map'; type Edit = WorkspaceFileEdit | WorkspaceTextEdit; @@ -257,10 +34,6 @@ class BulkEdit { edits: Edit[], @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, - @IFileService private readonly _fileService: IFileService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, - @IConfigurationService private readonly _configurationService: IConfigurationService ) { this._label = label; this._editor = editor; @@ -282,7 +55,7 @@ class BulkEdit { async perform(): Promise { - let seen = new Set(); + let seen = new ResourceMap(); let total = 0; const groups: Edit[][] = []; @@ -299,8 +72,8 @@ class BulkEdit { if (WorkspaceFileEdit.is(edit)) { total += 1; - } else if (!seen.has(edit.resource.toString())) { - seen.add(edit.resource.toString()); + } else if (!seen.has(edit.resource)) { + seen.set(edit.resource, true); total += 2; } } @@ -323,55 +96,14 @@ class BulkEdit { private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); - for (const edit of edits) { - progress.report(undefined); - - let options = edit.options || {}; - - if (edit.newUri && edit.oldUri) { - // rename - if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { - continue; // not overwriting, but ignoring, and the target file exists - } - await this._workingCopyFileService.move(edit.oldUri, edit.newUri, options.overwrite); - - } else if (!edit.newUri && edit.oldUri) { - // delete file - if (await this._fileService.exists(edit.oldUri)) { - let useTrash = this._configurationService.getValue('files.enableTrash'); - if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { - useTrash = false; // not supported by provider - } - await this._workingCopyFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); - } else if (!options.ignoreIfNotExists) { - throw new Error(`${edit.oldUri} does not exist and can not be deleted`); - } - } else if (edit.newUri && !edit.oldUri) { - // create file - if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { - continue; // not overwriting, but ignoring, and the target file exists - } - await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite }); - } - } + const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits); + await model.apply(); } private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); - - const model = this._instaService.createInstance(BulkEditModel, this._label, this._editor, progress, edits); - - await model.prepare(); - - // this._throwIfConflicts(conflicts); - const validationResult = model.validate(); - if (validationResult.canApply === false) { - model.dispose(); - throw new Error(`${validationResult.reason.toString()} has changed in the meantime`); - } - - model.apply(); - model.dispose(); + const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits); + await model.apply(); } } @@ -384,7 +116,6 @@ export class BulkEditService implements IBulkEditService { constructor( @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, - @IModelService private readonly _modelService: IModelService, @IEditorService private readonly _editorService: IEditorService, ) { } @@ -413,18 +144,6 @@ export class BulkEditService implements IBulkEditService { const { edits } = edit; let codeEditor = options?.editor; - - // First check if loaded models were not changed in the meantime - for (const edit of edits) { - if (!WorkspaceFileEdit.is(edit) && typeof edit.modelVersionId === 'number') { - let model = this._modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { - // model changed in the meantime - return Promise.reject(new Error(`${model.uri.toString()} has changed in the meantime`)); - } - } - } - // try to find code editor if (!codeEditor) { let candidate = this._editorService.activeTextEditorControl; diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts new file mode 100644 index 00000000000..f371ee227a4 --- /dev/null +++ b/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { WorkspaceFileEdit, WorkspaceFileEditOptions } from 'vs/editor/common/modes'; +import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkspaceUndoRedoElement, UndoRedoElementType, IResourceUndoRedoElement, IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +import { ILogService } from 'vs/platform/log/common/log'; +import { VSBuffer } from 'vs/base/common/buffer'; + +interface IFileOperation { + uris: URI[]; + perform(): Promise; +} + +class Noop implements IFileOperation { + readonly uris = []; + async perform() { return this; } +} + +class RenameOperation implements IFileOperation { + + constructor( + readonly newUri: URI, + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions, + @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, + @IFileService private readonly _fileService: IFileService, + ) { } + + get uris() { + return [this.newUri, this.oldUri]; + } + + async perform(): Promise { + // rename + if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { + return new Noop(); // not overwriting, but ignoring, and the target file exists + } + await this._workingCopyFileService.move(this.oldUri, this.newUri, this.options.overwrite); + return new RenameOperation(this.oldUri, this.newUri, this.options, this._workingCopyFileService, this._fileService); + } +} + +class CreateOperation implements IFileOperation { + + constructor( + readonly newUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly contents: VSBuffer | undefined, + @IFileService private readonly _fileService: IFileService, + @ITextFileService private readonly _textFileService: ITextFileService, + @IInstantiationService private readonly _instaService: IInstantiationService, + ) { } + + get uris() { + return [this.newUri]; + } + + async perform(): Promise { + // create file + if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { + return new Noop(); // not overwriting, but ignoring, and the target file exists + } + await this._textFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite }); + return this._instaService.createInstance(DeleteOperation, this.newUri, this.options); + } +} + +class DeleteOperation implements IFileOperation { + + constructor( + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions, + @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, + @IFileService private readonly _fileService: IFileService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService private readonly _instaService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { } + + get uris() { + return [this.oldUri]; + } + + async perform(): Promise { + // delete file + if (!await this._fileService.exists(this.oldUri)) { + if (!this.options.ignoreIfNotExists) { + throw new Error(`${this.oldUri} does not exist and can not be deleted`); + } + return new Noop(); + } + + let contents: VSBuffer | undefined; + try { + contents = (await this._fileService.readFile(this.oldUri)).value; + } catch (err) { + this._logService.critical(err); + } + + const useTrash = this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash'); + await this._workingCopyFileService.delete(this.oldUri, { useTrash, recursive: this.options.recursive }); + return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, contents); + } +} + +class FileUndoRedoElement implements IWorkspaceUndoRedoElement { + + readonly type = UndoRedoElementType.Workspace; + + readonly resources: readonly URI[] = []; + + constructor( + readonly label: string, + readonly operations: IFileOperation[] + ) { + // enable undo/redo here 👇 + // this.resources = ([]).concat(...operations.map(op => op.uris)); + } + + async undo(): Promise { + await this._reverse(); + } + + async redo(): Promise { + await this._reverse(); + } + + private async _reverse() { + for (let i = 0; i < this.operations.length; i++) { + const op = this.operations[i]; + const undo = await op.perform(); + this.operations[i] = undo; + } + } + + split(): IResourceUndoRedoElement[] { + return []; + } +} + +export class BulkFileEdits { + + constructor( + private readonly _label: string, + private readonly _progress: IProgress, + private readonly _edits: WorkspaceFileEdit[], + @IInstantiationService private readonly _instaService: IInstantiationService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + ) { } + + async apply(): Promise { + const undoOperations: IFileOperation[] = []; + for (const edit of this._edits) { + this._progress.report(undefined); + + const options = edit.options || {}; + let op: IFileOperation | undefined; + if (edit.newUri && edit.oldUri) { + // rename + op = this._instaService.createInstance(RenameOperation, edit.newUri, edit.oldUri, options); + } else if (!edit.newUri && edit.oldUri) { + // delete file + op = this._instaService.createInstance(DeleteOperation, edit.oldUri, options); + } else if (edit.newUri && !edit.oldUri) { + // create file + op = this._instaService.createInstance(CreateOperation, edit.newUri, options, undefined); + } + if (op) { + const undoOp = await op.perform(); + undoOperations.push(undoOp); + } + } + + this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations)); + } +} diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts new file mode 100644 index 00000000000..ca9dfa7739c --- /dev/null +++ b/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { mergeSort } from 'vs/base/common/arrays'; +import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; +import { ResourceMap } from 'vs/base/common/map'; +import { IModelService } from 'vs/editor/common/services/modelService'; + +type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; + +class ModelEditTask implements IDisposable { + + readonly model: ITextModel; + + private _expectedModelVersionId: number | undefined; + protected _edits: IIdentifiedSingleEditOperation[]; + protected _newEol: EndOfLineSequence | undefined; + + constructor(private readonly _modelReference: IReference) { + this.model = this._modelReference.object.textEditorModel; + this._edits = []; + } + + dispose() { + this._modelReference.dispose(); + } + + addEdit(resourceEdit: WorkspaceTextEdit): void { + this._expectedModelVersionId = resourceEdit.modelVersionId; + const { edit } = resourceEdit; + + if (typeof edit.eol === 'number') { + // honor eol-change + this._newEol = edit.eol; + } + if (!edit.range && !edit.text) { + // lacks both a range and the text + return; + } + if (Range.isEmpty(edit.range) && !edit.text) { + // no-op edit (replace empty range with empty text) + return; + } + + // create edit operation + let range: Range; + if (!edit.range) { + range = this.model.getFullModelRange(); + } else { + range = Range.lift(edit.range); + } + this._edits.push(EditOperation.replaceMove(range, edit.text)); + } + + validate(): ValidationResult { + if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) { + return { canApply: true }; + } + return { canApply: false, reason: this.model.uri }; + } + + getBeforeCursorState(): Selection[] | null { + return null; + } + + apply(): void { + if (this._edits.length > 0) { + this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + this.model.pushEditOperations(null, this._edits, () => null); + } + if (this._newEol !== undefined) { + this.model.pushEOL(this._newEol); + } + } +} + +class EditorEditTask extends ModelEditTask { + + private _editor: ICodeEditor; + + constructor(modelReference: IReference, editor: ICodeEditor) { + super(modelReference); + this._editor = editor; + } + + getBeforeCursorState(): Selection[] | null { + return this._editor.getSelections(); + } + + apply(): void { + if (this._edits.length > 0) { + this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + this._editor.executeEdits('', this._edits); + } + if (this._newEol !== undefined) { + if (this._editor.hasModel()) { + this._editor.getModel().pushEOL(this._newEol); + } + } + } +} + +export class BulkTextEdits { + + private readonly _edits = new ResourceMap(); + + constructor( + private readonly _label: string, + private readonly _editor: ICodeEditor | undefined, + private readonly _progress: IProgress, + edits: WorkspaceTextEdit[], + @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, + @IModelService private readonly _modelService: IModelService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService + ) { + + for (const edit of edits) { + let array = this._edits.get(edit.resource); + if (!array) { + array = []; + this._edits.set(edit.resource, array); + } + array.push(edit); + } + } + + private _validateBeforePrepare(): void { + // First check if loaded models were not changed in the meantime + for (const array of this._edits.values()) { + for (let edit of array) { + if (typeof edit.modelVersionId === 'number') { + let model = this._modelService.getModel(edit.resource); + if (model && model.getVersionId() !== edit.modelVersionId) { + // model changed in the meantime + throw new Error(`${model.uri.toString()} has changed in the meantime`); + } + } + } + } + } + + private async _createEditsTasks(): Promise { + + const tasks: ModelEditTask[] = []; + const promises: Promise[] = []; + + for (let [key, value] of this._edits) { + const promise = this._textModelResolverService.createModelReference(key).then(async ref => { + let task: ModelEditTask; + let makeMinimal = false; + if (this._editor?.getModel()?.uri.toString() === ref.object.textEditorModel.uri.toString()) { + task = new EditorEditTask(ref, this._editor); + makeMinimal = true; + } else { + task = new ModelEditTask(ref); + } + + for (const edit of value) { + if (makeMinimal) { + const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]); + if (!newEdits) { + task.addEdit(edit); + } else { + for (let moreMinialEdit of newEdits) { + task.addEdit({ ...edit, edit: moreMinialEdit }); + } + } + } else { + task.addEdit(edit); + } + } + + tasks.push(task); + this._progress.report(undefined); + }); + promises.push(promise); + } + + await Promise.all(promises); + return tasks; + } + + private _validateTasks(tasks: ModelEditTask[]): ValidationResult { + for (const task of tasks) { + const result = task.validate(); + if (!result.canApply) { + return result; + } + } + return { canApply: true }; + } + + async apply(): Promise { + + this._validateBeforePrepare(); + const tasks = await this._createEditsTasks(); + + try { + + const validation = this._validateTasks(tasks); + if (!validation.canApply) { + throw new Error(`${validation.reason.toString()} has changed in the meantime`); + } + if (tasks.length === 1) { + // This edit touches a single model => keep things simple + for (const task of tasks) { + task.model.pushStackElement(); + task.apply(); + task.model.pushStackElement(); + this._progress.report(undefined); + } + } else { + // prepare multi model undo element + const multiModelEditStackElement = new MultiModelEditStackElement( + this._label, + tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState())) + ); + this._undoRedoService.pushElement(multiModelEditStackElement); + for (const task of tasks) { + task.apply(); + this._progress.report(undefined); + } + multiModelEditStackElement.close(); + } + + } finally { + dispose(tasks); + } + } +} diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index b43d56fba26..13933070345 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -1807,7 +1807,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { return promise; }); - test('update remote settings', async () => { + test.skip('update remote settings', async () => { registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); await initialize(); diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 301187f332b..7ab5ba02ff5 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -208,7 +208,8 @@ class DecorationProviderWrapper { constructor( readonly provider: IDecorationsProvider, private readonly _uriEmitter: Emitter, - private readonly _flushEmitter: Emitter + private readonly _flushEmitter: Emitter, + @ILogService private readonly _logService: ILogService, ) { this._dispoable = this.provider.onDidChange(uris => { if (!uris) { @@ -238,16 +239,17 @@ class DecorationProviderWrapper { } getOrRetrieve(uri: URI, includeChildren: boolean, callback: (data: IDecorationData, isChild: boolean) => void): void { - let item = this.data.get(uri); if (item === undefined) { // unknown -> trigger request + this._logService.trace('[Decorations] getOrRetrieve -> FETCH', this.provider.label, uri); item = this._fetchData(uri); } if (item && !(item instanceof DecorationDataRequest)) { // found something (which isn't pending anymore) + this._logService.trace('[Decorations] getOrRetrieve -> RESULT', this.provider.label, uri); callback(item, false); } @@ -257,6 +259,7 @@ class DecorationProviderWrapper { if (iter) { for (let item = iter.next(); !item.done; item = iter.next()) { if (item.value && !(item.value instanceof DecorationDataRequest)) { + this._logService.trace('[Decorations] getOrRetrieve -> RESULT (children)', this.provider.label, uri); callback(item.value, true); } } @@ -269,6 +272,7 @@ class DecorationProviderWrapper { // check for pending request and cancel it const pendingRequest = this.data.get(uri); if (pendingRequest instanceof DecorationDataRequest) { + this._logService.trace('[Decorations] fetchData -> CANCEL previous', this.provider.label, uri); pendingRequest.source.cancel(); this.data.delete(uri); } @@ -297,6 +301,7 @@ class DecorationProviderWrapper { } private _keepItem(uri: URI, data: IDecorationData | undefined): IDecorationData | null { + this._logService.trace('[Decorations] keepItem -> CANCEL previous', this.provider.label, uri, data); const deco = data ? data : null; const old = this.data.set(uri, deco); if (deco || old) { @@ -343,7 +348,8 @@ export class DecorationsService implements IDecorationsService { const wrapper = new DecorationProviderWrapper( provider, this._onDidChangeDecorationsDelayed, - this._onDidChangeDecorations + this._onDidChangeDecorations, + this._logService ); const remove = this._data.push(wrapper); diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 15f0c38bb70..1260a837cd8 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -219,21 +219,17 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } private pickResource(options: IOpenDialogOptions): Promise { - const simpleFileDialog = this.createSimpleFileDialog(); + const simpleFileDialog = this.instantiationService.createInstance(SimpleFileDialog); return simpleFileDialog.showOpenDialog(options); } private saveRemoteResource(options: ISaveDialogOptions): Promise { - const remoteFileDialog = this.createSimpleFileDialog(); + const remoteFileDialog = this.instantiationService.createInstance(SimpleFileDialog); return remoteFileDialog.showSaveDialog(options); } - protected createSimpleFileDialog(): SimpleFileDialog { - return this.instantiationService.createInstance(SimpleFileDialog); - } - protected getSchemeFilterForWindow(): string { return !this.environmentService.configuration.remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME; } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index fd9ca23abcf..9b640a6b874 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -231,8 +231,8 @@ export class SimpleFileDialog { return this.remoteAgentEnvironment; } - protected async getUserHome(): Promise { - return (await this.pathService.userHome) ?? URI.from({ scheme: this.scheme, authority: this.remoteAuthority, path: '/' }); + protected getUserHome(): Promise { + return this.pathService.userHome({ preferLocal: this.scheme === Schemas.file }); } private async pickResource(isSave: boolean = false): Promise { diff --git a/src/vs/workbench/services/dialogs/electron-browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/electron-browser/simpleFileDialog.ts deleted file mode 100644 index ac98bcc8193..00000000000 --- a/src/vs/workbench/services/dialogs/electron-browser/simpleFileDialog.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vs/base/common/uri'; -import { SimpleFileDialog } from 'vs/workbench/services/dialogs/browser/simpleFileDialog'; -import { Schemas } from 'vs/base/common/network'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; - -export class NativeSimpleFileDialog extends SimpleFileDialog { - constructor( - @IFileService fileService: IFileService, - @IQuickInputService quickInputService: IQuickInputService, - @ILabelService labelService: ILabelService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @INotificationService notificationService: INotificationService, - @IFileDialogService fileDialogService: IFileDialogService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @IWorkbenchEnvironmentService protected environmentService: INativeWorkbenchEnvironmentService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IPathService protected pathService: IPathService, - @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - super(fileService, quickInputService, labelService, workspaceContextService, notificationService, fileDialogService, modelService, modeService, environmentService, remoteAgentService, pathService, keybindingService, contextKeyService); - } - - protected async getUserHome(): Promise { - if (this.scheme !== Schemas.file) { - return super.getUserHome(); - } - return this.environmentService.userHome; - } -} diff --git a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts similarity index 96% rename from src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts rename to src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts index 0bd77555b7b..f95983cf9c6 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts @@ -21,8 +21,6 @@ import { Schemas } from 'vs/base/common/network'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { ILabelService } from 'vs/platform/label/common/label'; -import { SimpleFileDialog } from 'vs/workbench/services/dialogs/browser/simpleFileDialog'; -import { NativeSimpleFileDialog } from 'vs/workbench/services/dialogs/electron-browser/simpleFileDialog'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { @@ -191,10 +189,6 @@ export class FileDialogService extends AbstractFileDialogService implements IFil // Don't allow untitled schema through. return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]); } - - protected createSimpleFileDialog(): SimpleFileDialog { - return this.instantiationService.createInstance(NativeSimpleFileDialog); - } } registerSingleton(IFileDialogService, FileDialogService, true); diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts new file mode 100644 index 00000000000..211960ac542 --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { isWeb } from 'vs/base/common/platform'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { URI } from 'vs/base/common/uri'; + +interface IScannedBuiltinExtension { + extensionPath: string, + packageJSON: any, + packageNLSPath?: string, + readmePath?: string, + changelogPath?: string, +} + +export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScannerService { + + declare readonly _serviceBrand: undefined; + + private readonly builtinExtensions: IScannedExtension[] = []; + + constructor( + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + ) { + + const builtinExtensionsServiceUrl = environmentService.options?.builtinExtensionsServiceUrl ? URI.parse(environmentService.options?.builtinExtensionsServiceUrl) : undefined; + if (isWeb && builtinExtensionsServiceUrl) { + + let scannedBuiltinExtensions: IScannedBuiltinExtension[] = []; + + if (environmentService.isBuilt) { + // Built time configuration (do NOT modify) + scannedBuiltinExtensions = [/*BUILD->INSERT_BUILTIN_EXTENSIONS*/]; + } else { + // Find builtin extensions by checking for DOM + const builtinExtensionsElement = document.getElementById('vscode-workbench-builtin-extensions'); + const builtinExtensionsElementAttribute = builtinExtensionsElement ? builtinExtensionsElement.getAttribute('data-settings') : undefined; + if (builtinExtensionsElementAttribute) { + try { + scannedBuiltinExtensions = JSON.parse(builtinExtensionsElementAttribute); + } catch (error) { /* ignore error*/ } + } + } + + this.builtinExtensions = scannedBuiltinExtensions.map(e => { + location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath), + type: ExtensionType.System, + packageJSON: e.packageJSON, + packageNLSUrl: e.packageNLSPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.packageNLSPath) : undefined, + readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, + changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, + }); + } + } + + async scanBuiltinExtensions(): Promise { + if (isWeb) { + return this.builtinExtensions; + } + throw new Error('not supported'); + } +} + +registerSingleton(IBuiltinExtensionsScannerService, BuiltinExtensionsScannerService); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts index 7948de2ce45..c28b1477400 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -18,6 +18,7 @@ import { getExtensionKind } from 'vs/workbench/services/extensions/common/extens import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { StorageManager } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -151,8 +152,13 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } if (extensionKind === 'web') { - // Web extensions are not yet supported to be disabled by kind. Enable them always on web. + const enableLocalWebWorker = this.configurationService.getValue(webWorkerExtHostConfig); + if (enableLocalWebWorker) { + // Web extensions are enabled on all configurations + return false; + } if (this.extensionManagementServerService.localExtensionManagementServer === null) { + // Web extensions run only in the web return false; } } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 5042393d735..afcf8322e31 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -14,11 +14,9 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { AbstractExtensionService, parseScannedExtension } from 'vs/workbench/services/extensions/common/abstractExtensionService'; -import { RemoteExtensionHost, IInitDataProvider } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; -import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; -import { URI } from 'vs/base/common/uri'; import { canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -31,7 +29,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot export class ExtensionService extends AbstractExtensionService implements IExtensionService { private _disposables = new DisposableStore(); - private _remoteExtensionsEnvironmentData: IRemoteAgentEnvironment | null = null; + private _remoteInitData: IRemoteExtensionHostInitData | null = null; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -71,14 +69,25 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._disposables.add(this._fileService.registerProvider(Schemas.https, provider)); } - private _createProvider(remoteAuthority: string): IInitDataProvider { + private _createLocalExtensionHostDataProvider() { + return { + getInitData: async () => { + const allExtensions = await this.getExtensions(); + const webExtensions = allExtensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService)); + return { + autoStart: true, + extensions: webExtensions + }; + } + }; + } + + private _createRemoteExtensionHostDataProvider(remoteAuthority: string): IRemoteExtensionHostDataProvider { return { remoteAuthority: remoteAuthority, getInitData: async () => { await this.whenInstalledExtensionsRegistered(); - const connectionData = this._remoteAuthorityResolverService.getConnectionData(remoteAuthority); - const remoteEnvironment = this._remoteExtensionsEnvironmentData!; - return { connectionData, remoteEnvironment }; + return this._remoteInitData!; } }; } @@ -86,14 +95,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten protected _createExtensionHosts(_isInitialStart: boolean): IExtensionHost[] { const result: IExtensionHost[] = []; - const webExtensions = this.getExtensions().then(extensions => extensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService))); - const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, webExtensions, URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme })); + const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, this._createLocalExtensionHostDataProvider()); result.push(webWorkerExtHost); const remoteAgentConnection = this._remoteAgentService.getConnection(); if (remoteAgentConnection) { - const remoteExtensions = this.getExtensions().then(extensions => extensions.filter(ext => !canExecuteOnWeb(ext, this._productService, this._configService))); - const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, remoteExtensions, this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); result.push(remoteExtHost); } @@ -107,13 +114,15 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension)) ]); + const remoteAgentConnection = this._remoteAgentService.getConnection(); + let result: DeltaExtensionsResult; // local: only enabled and web'ish extension localExtensions = localExtensions!.filter(ext => this._isEnabled(ext) && canExecuteOnWeb(ext, this._productService, this._configService)); this._checkEnableProposedApi(localExtensions); - if (!remoteEnv) { + if (!remoteEnv || !remoteAgentConnection) { result = this._registry.deltaExtensions(localExtensions, []); } else { @@ -127,7 +136,17 @@ export class ExtensionService extends AbstractExtensionService implements IExten localExtensions = localExtensions.filter(extension => !isRemoteExtension.has(ExtensionIdentifier.toKey(extension.identifier))); // save for remote extension's init data - this._remoteExtensionsEnvironmentData = remoteEnv; + this._remoteInitData = { + connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAgentConnection.remoteAuthority), + pid: remoteEnv.pid, + appRoot: remoteEnv.appRoot, + appSettingsHome: remoteEnv.appSettingsHome, + extensionHostLogsPath: remoteEnv.extensionHostLogsPath, + globalStorageHome: remoteEnv.globalStorageHome, + userHome: remoteEnv.userHome, + extensions: remoteEnv.extensions, + allExtensions: remoteEnv.extensions.concat(localExtensions) + }; result = this._registry.deltaExtensions(remoteEnv.extensions.concat(localExtensions), []); } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 6b9e01cc185..ad196030788 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -25,6 +25,15 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { localize } from 'vs/nls'; +export interface IWebWorkerExtensionHostInitData { + readonly autoStart: boolean; + readonly extensions: IExtensionDescription[]; +} + +export interface IWebWorkerExtensionHostDataProvider { + getInitData(): Promise; +} + export class WebWorkerExtensionHost implements IExtensionHost { public readonly kind = ExtensionHostKind.LocalWebWorker; @@ -37,11 +46,11 @@ export class WebWorkerExtensionHost implements IExtensionHost { private readonly _onDidExit = new Emitter<[number, string | null]>(); readonly onExit: Event<[number, string | null]> = this._onDidExit.event; + private readonly _extensionHostLogsLocation: URI; private readonly _extensionHostLogFile: URI; constructor( - private readonly _extensions: Promise, - private readonly _extensionHostLogsLocation: URI, + private readonly _initDataProvider: IWebWorkerExtensionHostDataProvider, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ILabelService private readonly _labelService: ILabelService, @@ -49,6 +58,7 @@ export class WebWorkerExtensionHost implements IExtensionHost { @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, ) { + this._extensionHostLogsLocation = URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme }); this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`); } @@ -129,7 +139,7 @@ export class WebWorkerExtensionHost implements IExtensionHost { } private async _createExtHostInitData(): Promise { - const [telemetryInfo, extensionDescriptions] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions]); + const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); return { commit: this._productService.commit, @@ -154,12 +164,12 @@ export class WebWorkerExtensionHost implements IExtensionHost { }, resolvedExtensions: [], hostExtensions: [], - extensions: extensionDescriptions, + extensions: initData.extensions, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._extensionHostLogsLocation, logFile: this._extensionHostLogFile, - autoStart: true, + autoStart: initData.autoStart, remote: { authority: this._environmentService.configuration.remoteAuthority, connectionData: null, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index d360eefba90..2fabfc1b08c 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -286,6 +286,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } + protected _checkEnabledAndProposedAPI(extensions: IExtensionDescription[]): IExtensionDescription[] { + // enable or disable proposed API per extension + this._checkEnableProposedApi(extensions); + + // keep only enabled extensions + return extensions.filter(extension => this._isEnabled(extension)); + } + private _isExtensionUnderDevelopment(extension: IExtensionDescription): boolean { if (this._environmentService.isExtensionDevelopment) { const extDevLocs = this._environmentService.extensionDevelopmentLocationURI; @@ -302,21 +310,17 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } protected _isEnabled(extension: IExtensionDescription): boolean { - return !this._isDisabled(extension); - } - - protected _isDisabled(extension: IExtensionDescription): boolean { if (this._isExtensionUnderDevelopment(extension)) { // Never disable extensions under development - return false; + return true; } if (ExtensionIdentifier.equals(extension.identifier, BetterMergeId)) { // Check if this is the better merge extension which was migrated to a built-in extension - return true; + return false; } - return !this._extensionEnablementService.isEnabled(toExtension(extension)); + return this._extensionEnablementService.isEnabled(toExtension(extension)); } protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index bd162d9d154..652a2603156 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -24,6 +24,8 @@ export const nullExtensionDescription = Object.freeze({ isBuiltin: false, }); +export const webWorkerExtHostConfig = 'extensions.webWorker'; + export const IExtensionService = createDecorator('extensionService'); export interface IMessage { diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 7cf595822ad..f91d97e80d8 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -15,7 +15,6 @@ import { IInitData, UIKind } from 'vs/workbench/api/common/extHost.protocol'; import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; -import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IRemoteAuthorityResolverService, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import * as platform from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; @@ -33,14 +32,21 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { localize } from 'vs/nls'; -export interface IRemoteInitData { +export interface IRemoteExtensionHostInitData { readonly connectionData: IRemoteConnectionData | null; - readonly remoteEnvironment: IRemoteAgentEnvironment; + readonly pid: number; + readonly appRoot: URI; + readonly appSettingsHome: URI; + readonly extensionHostLogsPath: URI; + readonly globalStorageHome: URI; + readonly userHome: URI; + readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; } -export interface IInitDataProvider { +export interface IRemoteExtensionHostDataProvider { readonly remoteAuthority: string; - getInitData(): Promise; + getInitData(): Promise; } export class RemoteExtensionHost extends Disposable implements IExtensionHost { @@ -56,8 +62,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { private readonly _isExtensionDevHost: boolean; constructor( - private readonly _allExtensions: Promise, - private readonly _initDataProvider: IInitDataProvider, + private readonly _initDataProvider: IRemoteExtensionHostDataProvider, private readonly _socketFactory: ISocketFactory, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @@ -196,53 +201,51 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { this._onExit.fire([0, null]); } - private _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise { - return Promise.all([this._allExtensions, this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]).then(([allExtensions, telemetryInfo, remoteInitData]) => { - // Collect all identifiers for extension ids which can be considered "resolved" - const resolvedExtensions = allExtensions.filter(extension => !extension.main).map(extension => extension.identifier); - const hostExtensions = allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier); - const workspace = this._contextService.getWorkspace(); - const remoteEnv = remoteInitData.remoteEnvironment; - const r: IInitData = { - commit: this._productService.commit, - version: this._productService.version, - parentPid: remoteEnv.pid, - environment: { - isExtensionDevelopmentDebug, - appRoot: remoteEnv.appRoot, - appSettingsHome: remoteEnv.appSettingsHome, - appName: this._productService.nameLong, - appUriScheme: this._productService.urlProtocol, - appLanguage: platform.language, - extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, - extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, - globalStorageHome: remoteEnv.globalStorageHome, - userHome: remoteEnv.userHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, - }, - workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : { - configuration: workspace.configuration, - id: workspace.id, - name: this._labelService.getWorkspaceLabel(workspace) - }, - remote: { - isRemote: true, - authority: this._initDataProvider.remoteAuthority, - connectionData: remoteInitData.connectionData - }, - resolvedExtensions: resolvedExtensions, - hostExtensions: hostExtensions, - extensions: remoteEnv.extensions, - telemetryInfo, - logLevel: this._logService.getLevel(), - logsLocation: remoteEnv.extensionHostLogsPath, - logFile: joinPath(remoteEnv.extensionHostLogsPath, `${ExtensionHostLogFileName}.log`), - autoStart: true, - uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop - }; - return r; - }); + private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise { + const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); + + // Collect all identifiers for extension ids which can be considered "resolved" + const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main).map(extension => extension.identifier); + const hostExtensions = remoteInitData.allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier); + const workspace = this._contextService.getWorkspace(); + return { + commit: this._productService.commit, + version: this._productService.version, + parentPid: remoteInitData.pid, + environment: { + isExtensionDevelopmentDebug, + appRoot: remoteInitData.appRoot, + appSettingsHome: remoteInitData.appSettingsHome, + appName: this._productService.nameLong, + appUriScheme: this._productService.urlProtocol, + appLanguage: platform.language, + extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, + extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, + globalStorageHome: remoteInitData.globalStorageHome, + userHome: remoteInitData.userHome, + webviewResourceRoot: this._environmentService.webviewResourceRoot, + webviewCspSource: this._environmentService.webviewCspSource, + }, + workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : { + configuration: workspace.configuration, + id: workspace.id, + name: this._labelService.getWorkspaceLabel(workspace) + }, + remote: { + isRemote: true, + authority: this._initDataProvider.remoteAuthority, + connectionData: remoteInitData.connectionData + }, + resolvedExtensions: resolvedExtensions, + hostExtensions: hostExtensions, + extensions: remoteInitData.extensions, + telemetryInfo, + logLevel: this._logService.getLevel(), + logsLocation: remoteInitData.extensionHostLogsPath, + logFile: joinPath(remoteInitData.extensionHostLogsPath, `${ExtensionHostLogFileName}.log`), + autoStart: true, + uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop + }; } getInspectPort(): number | undefined { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index bee57873c2b..b7efaf4f580 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -13,17 +13,16 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInitDataProvider, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; +import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost, webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; @@ -40,6 +39,8 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; class DeltaExtensionsQueueItem { constructor( @@ -50,8 +51,9 @@ class DeltaExtensionsQueueItem { export class ExtensionService extends AbstractExtensionService implements IExtensionService { - private readonly _remoteEnvironment: Map; - + private readonly _enableLocalWebWorker: boolean; + private readonly _remoteInitData: Map; + private _runningLocation: Map; private readonly _extensionScanner: CachedExtensionScanner; private _deltaExtensionsQueue: DeltaExtensionsQueueItem[]; @@ -84,6 +86,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten productService ); + this._enableLocalWebWorker = this._configurationService.getValue(webWorkerExtHostConfig); + if (this._extensionEnablementService.allUserExtensionsDisabled) { this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{ label: nls.localize('Reload', "Reload"), @@ -93,7 +97,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten }]); } - this._remoteEnvironment = new Map(); + this._remoteInitData = new Map(); + this._runningLocation = new Map(); this._extensionScanner = instantiationService.createInstance(CachedExtensionScanner); this._deltaExtensionsQueue = []; @@ -143,6 +148,14 @@ export class ExtensionService extends AbstractExtensionService implements IExten }); } + private _getExtensionHostManager(kind: ExtensionHostKind): ExtensionHostManager | null { + for (const extensionHostManager of this._extensionHostManagers) { + if (extensionHostManager.kind === kind) { + return extensionHostManager; + } + } + return null; + } //#region deltaExtensions @@ -221,11 +234,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._checkEnableProposedApi(toAdd); // Update extension points - this._rehandleExtensionPoints(([]).concat(toAdd).concat(toRemove)); + this._doHandleExtensionPoints(([]).concat(toAdd).concat(toRemove)); // Update the extension host - if (this._extensionHostManagers.length > 0) { - await this._extensionHostManagers[0].deltaExtensions(toAdd, toRemove.map(e => e.identifier)); + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHost) { + await localProcessExtensionHost.deltaExtensions(toAdd, toRemove.map(e => e.identifier)); } for (let i = 0; i < toAdd.length; i++) { @@ -233,15 +247,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } - private _rehandleExtensionPoints(extensionDescriptions: IExtensionDescription[]): void { - this._doHandleExtensionPoints(extensionDescriptions); - } - public canAddExtension(extensionDescription: IExtensionDescription): boolean { return this._canAddExtension(toExtension(extensionDescription)); } - public _canAddExtension(extension: IExtension): boolean { + private _canAddExtension(extension: IExtension): boolean { if (this._environmentService.configuration.remoteAuthority) { return false; } @@ -339,38 +349,61 @@ export class ExtensionService extends AbstractExtensionService implements IExten //#endregion - private _createProvider(remoteAuthority: string): IInitDataProvider { + private async _scanAllLocalExtensions(): Promise { + return flatten(await Promise.all([ + this._extensionScanner.scannedExtensions, + this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension)) + ])); + } + + private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation) { + return { + getInitData: async () => { + if (isInitialStart) { + const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); + const runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, [], false, this._enableLocalWebWorker); + const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); + return { + autoStart: false, + extensions: localProcessExtensions + }; + } else { + // restart case + const allExtensions = await this.getExtensions(); + const localProcessExtensions = filterByRunningLocation(allExtensions, this._runningLocation, desiredRunningLocation); + return { + autoStart: true, + extensions: localProcessExtensions + }; + } + } + }; + } + + private _createRemoteExtensionHostDataProvider(remoteAuthority: string): IRemoteExtensionHostDataProvider { return { remoteAuthority: remoteAuthority, getInitData: async () => { await this.whenInstalledExtensionsRegistered(); - const connectionData = this._remoteAuthorityResolverService.getConnectionData(remoteAuthority); - const remoteEnvironment = this._remoteEnvironment.get(remoteAuthority)!; - return { connectionData, remoteEnvironment }; + return this._remoteInitData.get(remoteAuthority)!; } }; } protected _createExtensionHosts(isInitialStart: boolean): IExtensionHost[] { - let autoStart: boolean; - let extensions: Promise; - if (isInitialStart) { - autoStart = false; - extensions = this._extensionScanner.scannedExtensions.then(extensions => extensions.filter(extension => this._isEnabled(extension))); // remove disabled extensions - } else { - // restart case - autoStart = true; - extensions = this.getExtensions().then((extensions) => extensions.filter(ext => ext.extensionLocation.scheme === Schemas.file)); - } - const result: IExtensionHost[] = []; - const localProcessExtHost = this._instantiationService.createInstance(LocalProcessExtensionHost, autoStart, extensions, this._environmentService.extHostLogsPath); + const localProcessExtHost = this._instantiationService.createInstance(LocalProcessExtensionHost, this._createLocalExtensionHostDataProvider(isInitialStart, ExtensionRunningLocation.LocalProcess)); result.push(localProcessExtHost); + if (this._enableLocalWebWorker) { + const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, this._createLocalExtensionHostDataProvider(isInitialStart, ExtensionRunningLocation.LocalWebWorker)); + result.push(webWorkerExtHost); + } + const remoteAgentConnection = this._remoteAgentService.getConnection(); if (remoteAgentConnection) { - const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); result.push(remoteExtHost); } @@ -429,10 +462,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten return; } - const extensionHost = this._extensionHostManagers[0]; + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); try { - const result = await extensionHost.resolveAuthority(remoteAuthority); + const result = await localProcessExtensionHost.resolveAuthority(remoteAuthority); this._remoteAuthorityResolverService._setResolvedAuthority(result.authority, result.options); } catch (err) { this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); @@ -443,28 +476,19 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._extensionScanner.startScanningExtensions(this.createLogger()); const remoteAuthority = this._environmentService.configuration.remoteAuthority; - const extensionHost = this._extensionHostManagers[0]; + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; - const allExtensions = flatten(await Promise.all([ - this._extensionScanner.scannedExtensions, - this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension)) - ])); - - // enable or disable proposed API per extension - this._checkEnableProposedApi(allExtensions); - - // remove disabled extensions - let localExtensions = remove(allExtensions, extension => this._isDisabled(extension)); + let localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); + let remoteEnv: IRemoteAgentEnvironment | null = null; if (remoteAuthority) { - let resolvedAuthority: ResolverResult; + let resolverResult: ResolverResult; try { - resolvedAuthority = await extensionHost.resolveAuthority(remoteAuthority); + resolverResult = await localProcessExtensionHost.resolveAuthority(remoteAuthority); } catch (err) { - const remoteName = getRemoteName(remoteAuthority); if (RemoteAuthorityResolverError.isNoResolverFound(err)) { - err.isHandled = await this._handleNoResolverFound(remoteName, allExtensions); + err.isHandled = await this._handleNoResolverFound(remoteAuthority); } else { console.log(err); if (RemoteAuthorityResolverError.isHandled(err)) { @@ -474,22 +498,18 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); // Proceed with the local extension host - await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier)); + await this._startLocalExtensionHost(localExtensions); return; } // set the resolved authority - this._remoteAuthorityResolverService._setResolvedAuthority(resolvedAuthority.authority, resolvedAuthority.options); - this._remoteExplorerService.setTunnelInformation(resolvedAuthority.tunnelInformation); + this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options); + this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation); // monitor for breakage const connection = this._remoteAgentService.getConnection(); if (connection) { connection.onDidStateChange(async (e) => { - const remoteAuthority = this._environmentService.configuration.remoteAuthority; - if (!remoteAuthority) { - return; - } if (e.type === PersistentConnectionEventType.ConnectionLost) { this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); } @@ -498,80 +518,64 @@ export class ExtensionService extends AbstractExtensionService implements IExten } // fetch the remote environment - const remoteEnv = (await this._remoteAgentService.getEnvironment()); + remoteEnv = await this._remoteAgentService.getEnvironment(); if (!remoteEnv) { this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") }); // Proceed with the local extension host - await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier)); + await this._startLocalExtensionHost(localExtensions); return; } - - // enable or disable proposed API per extension - this._checkEnableProposedApi(remoteEnv.extensions); - - // remove disabled extensions - remoteEnv.extensions = remove(remoteEnv.extensions, extension => this._isDisabled(extension)); - - // Determine where each extension will execute, based on extensionKind - const isInstalledLocally = new Set(); - localExtensions.forEach(ext => isInstalledLocally.add(ExtensionIdentifier.toKey(ext.identifier))); - - const isInstalledRemotely = new Set(); - remoteEnv.extensions.forEach(ext => isInstalledRemotely.add(ExtensionIdentifier.toKey(ext.identifier))); - - const enum RunningLocation { None, Local, Remote } - const pickRunningLocation = (extension: IExtensionDescription): RunningLocation => { - for (const extensionKind of getExtensionKind(extension, this._productService, this._configurationService)) { - if (extensionKind === 'ui') { - if (isInstalledLocally.has(ExtensionIdentifier.toKey(extension.identifier))) { - return RunningLocation.Local; - } - } else if (extensionKind === 'workspace') { - if (isInstalledRemotely.has(ExtensionIdentifier.toKey(extension.identifier))) { - return RunningLocation.Remote; - } - } - } - return RunningLocation.None; - }; - - const runningLocation = new Map(); - localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext))); - remoteEnv.extensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext))); - - // remove non-UI extensions from the local extensions - localExtensions = localExtensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === RunningLocation.Local); - - // in case of UI extensions overlap, the local extension wins - remoteEnv.extensions = remoteEnv.extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === RunningLocation.Remote); - - // save for remote extension's init data - this._remoteEnvironment.set(remoteAuthority, remoteEnv); - - await this._startLocalExtensionHost(extensionHost, remoteEnv.extensions.concat(localExtensions), localExtensions.map(extension => extension.identifier)); - } else { - await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier)); } + + await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv); } - private async _startLocalExtensionHost(extensionHost: ExtensionHostManager, allExtensions: IExtensionDescription[], localExtensions: ExtensionIdentifier[]): Promise { - this._registerAndHandleExtensions(allExtensions); - extensionHost.start(localExtensions.filter(id => this._registry.containsExtension(id))); - } + private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null): Promise { - private _registerAndHandleExtensions(allExtensions: IExtensionDescription[]): void { - const result = this._registry.deltaExtensions(allExtensions, []); + let remoteExtensions = remoteEnv ? this._checkEnabledAndProposedAPI(remoteEnv.extensions) : []; + + this._runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, remoteExtensions, Boolean(remoteAuthority), this._enableLocalWebWorker); + + // remove non-UI extensions from the local extensions + const localProcessExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalProcess); + const localWebWorkerExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); + remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote); + + const result = this._registry.deltaExtensions(remoteExtensions.concat(localProcessExtensions).concat(localWebWorkerExtensions), []); if (result.removedDueToLooping.length > 0) { this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); } + if (remoteAuthority && remoteEnv) { + this._remoteInitData.set(remoteAuthority, { + connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAuthority), + pid: remoteEnv.pid, + appRoot: remoteEnv.appRoot, + appSettingsHome: remoteEnv.appSettingsHome, + extensionHostLogsPath: remoteEnv.extensionHostLogsPath, + globalStorageHome: remoteEnv.globalStorageHome, + userHome: remoteEnv.userHome, + extensions: remoteExtensions, + allExtensions: this._registry.getAllExtensionDescriptions(), + }); + } + this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions()); + + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; + localProcessExtensionHost.start(localProcessExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + + const localWebWorkerExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalWebWorker); + if (localWebWorkerExtensionHost) { + localWebWorkerExtensionHost.start(localWebWorkerExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + } } public async getInspectPort(tryEnableInspector: boolean): Promise { - if (this._extensionHostManagers.length > 0) { - return this._extensionHostManagers[0].getInspectPort(tryEnableInspector); + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHost) { + return localProcessExtensionHost.getInspectPort(tryEnableInspector); } return 0; } @@ -586,7 +590,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } - private async _handleNoResolverFound(remoteName: string, allExtensions: IExtensionDescription[]): Promise { + private async _handleNoResolverFound(remoteAuthority: string): Promise { + const remoteName = getRemoteName(remoteAuthority); const recommendation = this._productService.remoteExtensionTips?.[remoteName]; if (!recommendation) { return false; @@ -602,9 +607,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten }; const resolverExtensionId = recommendation.extensionId; + const allExtensions = await this._scanAllLocalExtensions(); const extension = allExtensions.filter(e => e.identifier.value === resolverExtensionId)[0]; if (extension) { - if (this._isDisabled(extension)) { + if (!this._isEnabled(extension)) { const message = nls.localize('enableResolver', "Extension '{0}' is required to open the remote window.\nOK to enable?", recommendation.friendlyName); this._notificationService.prompt(Severity.Info, message, [{ @@ -644,27 +650,59 @@ export class ExtensionService extends AbstractExtensionService implements IExten } return true; - } } -function remove(arr: IExtensionDescription[], predicate: (item: IExtensionDescription) => boolean): IExtensionDescription[]; -function remove(arr: IExtensionDescription[], toRemove: IExtensionDescription[]): IExtensionDescription[]; -function remove(arr: IExtensionDescription[], arg2: ((item: IExtensionDescription) => boolean) | IExtensionDescription[]): IExtensionDescription[] { - if (typeof arg2 === 'function') { - return _removePredicate(arr, arg2); - } - return _removeSet(arr, arg2); +const enum ExtensionRunningLocation { + None, + LocalProcess, + LocalWebWorker, + Remote } -function _removePredicate(arr: IExtensionDescription[], predicate: (item: IExtensionDescription) => boolean): IExtensionDescription[] { - return arr.filter(extension => !predicate(extension)); +function determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean, hasLocalWebWorker: boolean): Map { + const localExtensionsSet = new Set(); + localExtensions.forEach(ext => localExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier))); + + const remoteExtensionsSet = new Set(); + remoteExtensions.forEach(ext => remoteExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier))); + + const pickRunningLocation = (extension: IExtensionDescription): ExtensionRunningLocation => { + const isInstalledLocally = localExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier)); + const isInstalledRemotely = remoteExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier)); + for (const extensionKind of getExtensionKind(extension, productService, configurationService)) { + if (extensionKind === 'ui' && isInstalledLocally) { + // ui extensions run locally if possible + return ExtensionRunningLocation.LocalProcess; + } + if (extensionKind === 'workspace' && isInstalledRemotely) { + // workspace extensions run remotely if possible + return ExtensionRunningLocation.Remote; + } + if (extensionKind === 'workspace' && !hasRemote) { + // workspace extensions also run locally if there is no remote + return ExtensionRunningLocation.LocalProcess; + } + if (extensionKind === 'web' && isInstalledLocally && hasLocalWebWorker) { + // web worker extensions run in the local web worker if possible + if (typeof extension.browser !== 'undefined') { + // The "browser" field determines the entry point + (extension).main = extension.browser; + } + return ExtensionRunningLocation.LocalWebWorker; + } + } + return ExtensionRunningLocation.None; + }; + + const runningLocation = new Map(); + localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext))); + remoteExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext))); + return runningLocation; } -function _removeSet(arr: IExtensionDescription[], toRemove: IExtensionDescription[]): IExtensionDescription[] { - const toRemoveSet = new Set(); - toRemove.forEach(extension => toRemoveSet.add(ExtensionIdentifier.toKey(extension.identifier))); - return arr.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier))); +function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { + return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation); } registerSingleton(IExtensionService, ExtensionService); diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index e45eded864f..172a8c6817f 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -45,6 +45,15 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +export interface ILocalProcessExtensionHostInitData { + readonly autoStart: boolean; + readonly extensions: IExtensionDescription[]; +} + +export interface ILocalProcessExtensionHostDataProvider { + getInitData(): Promise; +} + export class LocalProcessExtensionHost implements IExtensionHost { public readonly kind = ExtensionHostKind.LocalProcess; @@ -76,9 +85,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private readonly _extensionHostLogFile: URI; constructor( - private readonly _autoStart: boolean, - private readonly _extensions: Promise, - private readonly _extensionHostLogsLocation: URI, + private readonly _initDataProvider: ILocalProcessExtensionHostDataProvider, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @INotificationService private readonly _notificationService: INotificationService, @IElectronService private readonly _electronService: IElectronService, @@ -106,7 +113,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._extensionHostConnection = null; this._messageProtocol = null; - this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`); + this._extensionHostLogFile = joinPath(this._environmentService.extHostLogsPath, `${ExtensionHostLogFileName}.log`); this._toDispose.add(this._onExit); this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); @@ -413,51 +420,48 @@ export class LocalProcessExtensionHost implements IExtensionHost { }); } - private _createExtHostInitData(): Promise { - return Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions]) - .then(([telemetryInfo, extensionDescriptions]) => { - const workspace = this._contextService.getWorkspace(); - const r: IInitData = { - commit: this._productService.commit, - version: this._productService.version, - parentPid: process.pid, - environment: { - isExtensionDevelopmentDebug: this._isExtensionDevDebug, - appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, - appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined, - appName: this._productService.nameLong, - appUriScheme: this._productService.urlProtocol, - appLanguage: platform.language, - extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, - extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, - globalStorageHome: URI.file(this._environmentService.globalStorageHome), - userHome: this._environmentService.userHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, - }, - workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { - configuration: withNullAsUndefined(workspace.configuration), - id: workspace.id, - name: this._labelService.getWorkspaceLabel(workspace), - isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false - }, - remote: { - authority: this._environmentService.configuration.remoteAuthority, - connectionData: null, - isRemote: false - }, - resolvedExtensions: [], - hostExtensions: [], - extensions: extensionDescriptions, - telemetryInfo, - logLevel: this._logService.getLevel(), - logsLocation: this._extensionHostLogsLocation, - logFile: this._extensionHostLogFile, - autoStart: this._autoStart, - uiKind: UIKind.Desktop - }; - return r; - }); + private async _createExtHostInitData(): Promise { + const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); + const workspace = this._contextService.getWorkspace(); + return { + commit: this._productService.commit, + version: this._productService.version, + parentPid: process.pid, + environment: { + isExtensionDevelopmentDebug: this._isExtensionDevDebug, + appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, + appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined, + appName: this._productService.nameLong, + appUriScheme: this._productService.urlProtocol, + appLanguage: platform.language, + extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, + extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, + globalStorageHome: URI.file(this._environmentService.globalStorageHome), + userHome: this._environmentService.userHome, + webviewResourceRoot: this._environmentService.webviewResourceRoot, + webviewCspSource: this._environmentService.webviewCspSource, + }, + workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { + configuration: withNullAsUndefined(workspace.configuration), + id: workspace.id, + name: this._labelService.getWorkspaceLabel(workspace), + isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false + }, + remote: { + authority: this._environmentService.configuration.remoteAuthority, + connectionData: null, + isRemote: false + }, + resolvedExtensions: [], + hostExtensions: [], + extensions: initData.extensions, + telemetryInfo, + logLevel: this._logService.getLevel(), + logsLocation: this._environmentService.extHostLogsPath, + logFile: this._extensionHostLogFile, + autoStart: initData.autoStart, + uiKind: UIKind.Desktop + }; } private _logExtensionHostMessage(entry: IRemoteConsoleLog) { diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index d3abef54196..c4f72f05518 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -283,7 +283,7 @@ export class BrowserHostService extends Disposable implements IHostService { } private async doOpenEmptyWindow(options?: IOpenEmptyWindowOptions): Promise { - this.workspaceProvider.open(undefined, { reuse: options?.forceReuseWindow }); + return this.workspaceProvider.open(undefined, { reuse: options?.forceReuseWindow }); } async toggleFullScreen(): Promise { diff --git a/src/vs/workbench/services/path/common/pathService.ts b/src/vs/workbench/services/path/common/pathService.ts index 3056dbd901c..c2e1b21bd5a 100644 --- a/src/vs/workbench/services/path/common/pathService.ts +++ b/src/vs/workbench/services/path/common/pathService.ts @@ -40,13 +40,13 @@ export interface IPathService { /** * Resolves the user-home directory for the target environment. * If the envrionment is connected to a remote, this will be the - * remote's user home directory, otherwise the local one. + * remote's user home directory, otherwise the local one unless + * `preferLocal` is set to `true`. */ - readonly userHome: Promise; + userHome(options?: { preferLocal: boolean }): Promise; /** - * Access to `userHome` in a sync fashion. This may be `undefined` - * as long as the remote environment was not resolved. + * @deprecated use `userHome` instead. */ readonly resolvedUserHome: URI | undefined; } @@ -55,26 +55,35 @@ export abstract class AbstractPathService implements IPathService { declare readonly _serviceBrand: undefined; - private remoteOS: Promise; + private resolveOS: Promise; private resolveUserHome: Promise; private maybeUnresolvedUserHome: URI | undefined; constructor( - fallbackUserHome: URI, + private localUserHome: URI, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService ) { - this.remoteOS = this.remoteAgentService.getEnvironment().then(env => env?.os || OS); - this.resolveUserHome = this.remoteAgentService.getEnvironment().then(env => { - const userHome = this.maybeUnresolvedUserHome = env?.userHome || fallbackUserHome; + // OS + this.resolveOS = (async () => { + const env = await this.remoteAgentService.getEnvironment(); + + return env?.os || OS; + })(); + + // User Home + this.resolveUserHome = (async () => { + const env = await this.remoteAgentService.getEnvironment(); + const userHome = this.maybeUnresolvedUserHome = env?.userHome || localUserHome; + return userHome; - }); + })(); } - get userHome(): Promise { - return this.resolveUserHome; + async userHome(options?: { preferLocal: boolean }): Promise { + return options?.preferLocal ? this.localUserHome : this.resolveUserHome; } get resolvedUserHome(): URI | undefined { @@ -82,7 +91,7 @@ export abstract class AbstractPathService implements IPathService { } get path(): Promise { - return this.remoteOS.then(os => { + return this.resolveOS.then(os => { return os === OperatingSystem.Windows ? win32 : posix; @@ -95,7 +104,8 @@ export abstract class AbstractPathService implements IPathService { // normalize to fwd-slashes on windows, // on other systems bwd-slashes are valid // filename character, eg /f\oo/ba\r.txt - if ((await this.remoteOS) === OperatingSystem.Windows) { + const os = await this.resolveOS; + if (os === OperatingSystem.Windows) { _path = _path.replace(/\\/g, '/'); } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index fabf89c0b32..58328694581 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -152,7 +152,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } } - async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise { + async create(resource: URI, value?: string | ITextSnapshot | VSBuffer, options?: ICreateFileOptions): Promise { // file operation participation await this.workingCopyFileService.runFileOperationParticipants(resource, undefined, FileOperation.CREATE); @@ -175,8 +175,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return stat; } - protected doCreate(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise { - return this.fileService.createFile(resource, toBufferOrReadable(value), options); + protected doCreate(resource: URI, value?: string | ITextSnapshot | VSBuffer, options?: ICreateFileOptions): Promise { + return this.fileService.createFile(resource, value instanceof VSBuffer ? value : toBufferOrReadable(value), options); } async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { @@ -462,7 +462,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // Try to place where last active file was if any // Otherwise fallback to user home - return joinPath(this.fileDialogService.defaultFilePath() || (await this.pathService.userHome), suggestedFilename); + return joinPath(this.fileDialogService.defaultFilePath() || (await this.pathService.userHome()), suggestedFilename); } //#endregion diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index e4528b1e8b6..48a490bb487 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -104,7 +104,7 @@ export interface ITextFileService extends IDisposable { * Create a file. If the file exists it will be overwritten with the contents if * the options enable to overwrite. */ - create(resource: URI, contents?: string | ITextSnapshot, options?: { overwrite?: boolean }): Promise; + create(resource: URI, contents?: string | ITextSnapshot | VSBuffer, options?: { overwrite?: boolean }): Promise; } export interface IReadTextFileOptions extends IReadFileOptions { diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 9f00df45232..062219d1e51 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -100,7 +100,7 @@ suite('Files - TextFileService', () => { assert.ok(!accessor.textFileService.isDirty(model.resource)); }); - test('create', async function () { + test('create does not overwrite existing model', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); (accessor.textFileService.files).add(model.resource, model); diff --git a/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts index a5558692e2f..0690e162cda 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Schemas } from 'vs/base/common/network'; @@ -27,6 +27,7 @@ import { isWindows } from 'vs/base/common/platform'; import { readFileSync, statSync } from 'fs'; import { detectEncodingByBOM } from 'vs/workbench/services/textfile/test/node/encoding/encoding.test'; import { workbenchInstantiationService, TestNativeTextFileServiceWithEncodingOverrides } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { VSBuffer } from 'vs/base/common/buffer'; suite('Files - TextFileService i/o', function () { const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice'); @@ -82,7 +83,7 @@ suite('Files - TextFileService i/o', function () { assert.equal(await exists(resource.fsPath), true); }); - test('create - no encoding - content provided', async () => { + test('create - no encoding - content provided (string)', async () => { const resource = URI.file(join(testDir, 'small_new.txt')); await service.create(resource, 'Hello World'); @@ -91,6 +92,24 @@ suite('Files - TextFileService i/o', function () { assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World'); }); + test('create - no encoding - content provided (snapshot)', async () => { + const resource = URI.file(join(testDir, 'small_new.txt')); + + await service.create(resource, stringToSnapshot('Hello World')); + + assert.equal(await exists(resource.fsPath), true); + assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World'); + }); + + test('create - no encoding - content provided (VSBuffer)', async () => { + const resource = URI.file(join(testDir, 'small_new.txt')); + + await service.create(resource, VSBuffer.fromString('Hello World')); + + assert.equal(await exists(resource.fsPath), true); + assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World'); + }); + test('create - UTF 16 LE - no content', async () => { const resource = URI.file(join(testDir, 'small_new.utf16le')); diff --git a/src/vs/workbench/test/browser/api/extHostDecorations.test.ts b/src/vs/workbench/test/browser/api/extHostDecorations.test.ts new file mode 100644 index 00000000000..073006e636d --- /dev/null +++ b/src/vs/workbench/test/browser/api/extHostDecorations.test.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { MainThreadDecorationsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; + +suite('ExtHostDecorations', function () { + + let mainThreadShape: MainThreadDecorationsShape; + let extHostDecorations: ExtHostDecorations; + let providers = new Set(); + + setup(function () { + + providers.clear(); + + mainThreadShape = new class extends mock() { + $registerDecorationProvider(handle: number) { + providers.add(handle); + } + }; + + extHostDecorations = new ExtHostDecorations( + new class extends mock() { + getProxy(): any { + return mainThreadShape; + } + }, + new NullLogService() + ); + }); + + test('SCM Decorations missing #100524', async function () { + + let calledA = false; + let calledB = false; + + // never returns + extHostDecorations.registerDecorationProvider({ + onDidChangeDecorations: Event.None, + provideDecoration() { + calledA = true; + return new Promise(() => { }); + } + }, nullExtensionDescription.identifier); + + // always returns + extHostDecorations.registerDecorationProvider({ + onDidChangeDecorations: Event.None, + provideDecoration() { + calledB = true; + return new Promise(resolve => resolve({ letter: 'H', title: 'Hello' })); + } + }, nullExtensionDescription.identifier); + + + const requests = [...providers.values()].map((handle, idx) => { + return extHostDecorations.$provideDecorations(handle, [{ id: idx, uri: URI.parse('test:///file') }], CancellationToken.None); + }); + + assert.equal(calledA, true); + assert.equal(calledB, true); + + assert.equal(requests.length, 2); + const [first, second] = requests; + + const firstResult = await Promise.race([first, timeout(30).then(() => false)]); + assert.equal(typeof firstResult, 'boolean'); // never finishes... + + const secondResult = await Promise.race([second, timeout(30).then(() => false)]); + assert.equal(typeof secondResult, 'object'); + }); + +}); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 48cca5bef2e..1449ad50c1f 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1154,7 +1154,7 @@ export class TestPathService implements IPathService { get path() { return Promise.resolve(isWindows ? win32 : posix); } - get userHome() { return Promise.resolve(this.fallbackUserHome); } + async userHome() { return this.fallbackUserHome; } get resolvedUserHome() { return this.fallbackUserHome; } async fileURI(path: string): Promise { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index fa905653d67..71b4a2d9faf 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -75,6 +75,7 @@ import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensionManagement/common/webExtensionsScannerService'; import 'vs/workbench/services/extensionManagement/common/extensionEnablementService'; +import 'vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; import 'vs/workbench/services/remote/common/remoteExplorerService'; @@ -90,8 +91,6 @@ import 'vs/workbench/services/hover/browser/hoverService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { IBuiltinExtensionsScannerService } from 'vs/platform/extensions/common/extensions'; -import { BuiltinExtensionsScannerService } from 'vs/platform/extensions/browser/builtinExtensionsScannerService'; import { IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -117,7 +116,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; -registerSingleton(IBuiltinExtensionsScannerService, BuiltinExtensionsScannerService); registerSingleton(IUserDataSyncResourceEnablementService, UserDataSyncResourceEnablementService); registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService); registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 8d953960da1..8399a2ea199 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -34,7 +34,6 @@ import 'vs/workbench/electron-browser/desktop.main'; //#region --- workbench services -import 'vs/workbench/services/dialogs/electron-browser/fileDialogService'; import 'vs/workbench/services/integrity/node/integrityService'; import 'vs/workbench/services/textMate/electron-browser/textMateService'; import 'vs/workbench/services/search/node/searchService'; @@ -44,7 +43,6 @@ import 'vs/workbench/services/dialogs/electron-browser/dialogService'; import 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import 'vs/workbench/services/keybinding/electron-browser/keybinding.contribution'; import 'vs/workbench/services/extensions/electron-browser/extensionService'; -import 'vs/workbench/services/contextmenu/electron-sandbox/contextmenuService'; // TODO@Miguel this cannot be moved to sandbox due to https://github.com/microsoft/vscode/issues/98495 import 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-browser/extensionTipsService'; import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 318f2a7502e..4ab428b3565 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -19,6 +19,7 @@ import 'vs/workbench/workbench.common.main'; //#region --- workbench services +import 'vs/workbench/services/dialogs/electron-sandbox/fileDialogService'; import 'vs/workbench/services/workspaces/electron-sandbox/workspacesService'; import 'vs/workbench/services/userDataSync/electron-sandbox/storageKeysSyncRegistryService'; import 'vs/workbench/services/menubar/electron-sandbox/menubarService'; @@ -31,7 +32,7 @@ import 'vs/workbench/services/host/electron-sandbox/desktopHostService'; import 'vs/workbench/services/request/electron-sandbox/requestService'; import 'vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService'; import 'vs/workbench/services/clipboard/electron-sandbox/clipboardService'; - +import 'vs/workbench/services/contextmenu/electron-sandbox/contextmenuService'; //#endregion diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index e2be11afcc1..05124a2197b 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -260,6 +260,11 @@ interface IWorkbenchConstructionOptions { */ readonly staticExtensions?: ReadonlyArray; + /** + * Service end-point hosting builtin extensions + */ + readonly builtinExtensionsServiceUrl?: string; + /** * Support for URL callbacks. */ diff --git a/yarn.lock b/yarn.lock index 11bddedb357..c803695e4e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9102,10 +9102,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.0.0-dev.20200615: - version "4.0.0-dev.20200615" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200615.tgz#5c06a0d5f25a29a018767970c6531fbbed7240e3" - integrity sha512-OD7KRTLimUwW5E1xHsAqXNjw0O0Krk9CgRVFYkqANv4fZisaN1LJI06u30D5QiNnHBzm2nBSzZIAhjj4MUqaRA== +typescript@^4.0.0-dev.20200622: + version "4.0.0-dev.20200622" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.0-dev.20200622.tgz#33e0ffaf880b1f16bde5bc4eeb1863e52c4d7f75" + integrity sha512-KWXppG2OKfq5cDAEkc0wA7uemXnF/Af4v0j08plUCKk20rt9wYU2rU9EB53/XlVeZgV2hwpbH9hIFyeB4dWvdg== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"